From b1ca1a748efebcd3d04b58d4705e0310c0353fa7 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:22:42 -0600 Subject: [PATCH] Let decision-approaches wrap at narrow widths and sync key-resource scopes. The info-box checkboxes overflowed in a squeezed column; wrapping them and offering those scopes as chips on every approach keeps the sidebar and Applicable Scope selection in sync without publishing unchecked keys as defaults. Co-authored-by: Cursor --- .../components/ApplicableScopeField.tsx | 1 + .../components/FinalReviewChipEditModal.tsx | 17 +- .../DecisionApproachEditFields.tsx | 10 +- .../right-rail/DecisionApproachesScreen.tsx | 98 +++++++++--- app/(app)/create/types.ts | 6 + .../cards/Selection/Selection.view.tsx | 6 +- .../controls/Checkbox/Checkbox.view.tsx | 4 +- app/components/controls/Chip/Chip.view.tsx | 3 +- .../InfoMessageBox/InfoMessageBox.view.tsx | 38 ++--- lib/create/customRuleFacets.ts | 5 +- lib/create/decisionApproachKeyResources.ts | 149 ++++++++++++++++++ .../methodCardFacetMatchesPresetForId.ts | 11 +- .../ruleSectionsFromMethodSelections.ts | 12 +- lib/server/validation/createFlowSchemas.ts | 1 + tests/components/InfoMessageBox.test.tsx | 7 + tests/pages/decision-approaches.test.jsx | 87 ++++++++++ .../unit/decisionApproachKeyResources.test.ts | 137 ++++++++++++++++ .../methodCardFacetMatchesPresetForId.test.ts | 29 +++- .../stripCustomRuleSelectionFields.test.ts | 2 + 19 files changed, 567 insertions(+), 56 deletions(-) create mode 100644 lib/create/decisionApproachKeyResources.ts create mode 100644 tests/unit/decisionApproachKeyResources.test.ts diff --git a/app/(app)/create/components/ApplicableScopeField.tsx b/app/(app)/create/components/ApplicableScopeField.tsx index a1bf68d..bdb0635 100644 --- a/app/(app)/create/components/ApplicableScopeField.tsx +++ b/app/(app)/create/components/ApplicableScopeField.tsx @@ -84,6 +84,7 @@ function ApplicableScopeFieldComponent({ disabled={readOnly} onClick={() => !readOnly && onToggleScope(scope)} ariaLabel={`${isSelected ? "Deselect" : "Select"} ${scope}`} + className="max-w-full" /> ); })} diff --git a/app/(app)/create/components/FinalReviewChipEditModal.tsx b/app/(app)/create/components/FinalReviewChipEditModal.tsx index 266f739..4da16bb 100644 --- a/app/(app)/create/components/FinalReviewChipEditModal.tsx +++ b/app/(app)/create/components/FinalReviewChipEditModal.tsx @@ -35,6 +35,10 @@ import { decisionApproachPresetFor, membershipPresetFor, } from "../../../../lib/create/finalReviewChipPresets"; +import { + applyDecisionApproachKeyResources, + selectedKeyResourceLabelsFromCheckedIds, +} from "../../../../lib/create/decisionApproachKeyResources"; import { isCustomMethodCardId } from "../../../../lib/create/isCustomMethodCardId"; import { usesWizardFieldBlocksModalBody } from "../../../../lib/create/usesWizardFieldBlocksModalBody"; import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks"; @@ -1266,6 +1270,7 @@ function facetSeedSignature( return JSON.stringify({ meta: state.customMethodCardMetaById?.[id] ?? null, details: state.decisionApproachDetailsById?.[id] ?? null, + keyResources: state.selectedDecisionKeyResourceIds ?? null, blocks: state.customMethodCardFieldBlocksById?.[id] ?? null, }); case "conflictManagement": @@ -1330,12 +1335,18 @@ function seedDraftForTarget( const saved = state.decisionApproachDetailsById?.[target.overrideKey] ?? decisionApproachPresetFor(target.overrideKey); + const withKeys = applyDecisionApproachKeyResources( + saved, + selectedKeyResourceLabelsFromCheckedIds( + state.selectedDecisionKeyResourceIds ?? [], + ), + ); return { groupKey: "decisionApproaches", value: { - ...saved, - applicableScope: [...saved.applicableScope], - selectedApplicableScope: [...saved.selectedApplicableScope], + ...withKeys, + applicableScope: [...withKeys.applicableScope], + selectedApplicableScope: [...withKeys.selectedApplicableScope], }, }; } diff --git a/app/(app)/create/components/methodEditFields/DecisionApproachEditFields.tsx b/app/(app)/create/components/methodEditFields/DecisionApproachEditFields.tsx index 62b67c3..9c1de98 100644 --- a/app/(app)/create/components/methodEditFields/DecisionApproachEditFields.tsx +++ b/app/(app)/create/components/methodEditFields/DecisionApproachEditFields.tsx @@ -7,12 +7,13 @@ * `markCreateFlowInteraction` live in the parent. */ -import { memo, useCallback } from "react"; +import { memo, useCallback, useMemo } from "react"; import { useMessages } from "../../../../contexts/MessagesContext"; import ModalTextAreaField from "../ModalTextAreaField"; import ApplicableScopeField from "../ApplicableScopeField"; import IncrementerBlock from "../../../../components/controls/IncrementerBlock"; import type { DecisionApproachDetailEntry } from "../../types"; +import { withDecisionApproachKeyResourceScopes } from "../../../../../lib/create/decisionApproachKeyResources"; export interface DecisionApproachEditFieldsProps { value: DecisionApproachDetailEntry; @@ -32,6 +33,11 @@ function DecisionApproachEditFieldsComponent({ const m = useMessages(); const t = m.create.customRule.decisionApproaches; + const scopes = useMemo( + () => withDecisionApproachKeyResourceScopes(value.applicableScope), + [value.applicableScope], + ); + const patch = useCallback( ( key: K, @@ -53,7 +59,7 @@ function DecisionApproachEditFieldsComponent({ diff --git a/app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx b/app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx index 69c3a76..4f43712 100644 --- a/app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx +++ b/app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx @@ -33,6 +33,13 @@ import { DecisionApproachEditFields } from "../../components/methodEditFields"; import CustomMethodCardWizard from "../../components/CustomMethodCardWizard"; import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer"; import { decisionApproachPresetFor } from "../../../../../lib/create/finalReviewChipPresets"; +import { + applyDecisionApproachKeyResources, + decisionApproachKeyResourceIdsFromLabels, + selectedKeyResourceLabelsFromCheckedIds, + stringArraysEqual, + syncDecisionApproachKeyResourceDetails, +} from "../../../../../lib/create/decisionApproachKeyResources"; import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks"; import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom"; import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder"; @@ -71,9 +78,6 @@ export function DecisionApproachesScreen() { const customizeSnapshotRef = useRef< MethodCardCustomizeSnapshot | null >(null); - const [messageBoxCheckedIds, setMessageBoxCheckedIds] = useState( - [], - ); const [expanded, setExpanded] = useState(false); const [createModalOpen, setCreateModalOpen] = useState(false); const [pendingCardId, setPendingCardId] = useState(null); @@ -90,6 +94,11 @@ export function DecisionApproachesScreen() { >(null); const selectedIds = state.selectedDecisionApproachIds ?? []; + const messageBoxCheckedIds = state.selectedDecisionKeyResourceIds ?? []; + const selectedKeyResourceLabels = useMemo( + () => selectedKeyResourceLabelsFromCheckedIds(messageBoxCheckedIds), + [messageBoxCheckedIds], + ); const messageBoxItems: InfoMessageBoxItem[] = useMemo( () => @@ -137,26 +146,52 @@ export function DecisionApproachesScreen() { const handleMessageBoxCheckboxChange = useCallback( (id: string, checked: boolean) => { markCreateFlowInteraction(); - setMessageBoxCheckedIds((prev) => - checked ? [...prev, id] : prev.filter((x) => x !== id), - ); + const nextCheckedIds = checked + ? [...messageBoxCheckedIds, id] + : messageBoxCheckedIds.filter((x) => x !== id); + const nextLabels = + selectedKeyResourceLabelsFromCheckedIds(nextCheckedIds); + if (pendingDraft) { + setPendingDraft( + applyDecisionApproachKeyResources(pendingDraft, nextLabels), + ); + } + updateState({ + selectedDecisionKeyResourceIds: nextCheckedIds, + decisionApproachDetailsById: syncDecisionApproachKeyResourceDetails( + state.decisionApproachDetailsById, + selectedIds, + nextLabels, + decisionApproachPresetFor, + ), + }); }, - [markCreateFlowInteraction], + [ + markCreateFlowInteraction, + messageBoxCheckedIds, + pendingDraft, + selectedIds, + state.decisionApproachDetailsById, + updateState, + ], ); const seedDraft = useCallback( (id: string): DecisionApproachDetailEntry => { const saved = state.decisionApproachDetailsById?.[id]; - if (saved) { - return { - ...saved, - applicableScope: [...saved.applicableScope], - selectedApplicableScope: [...saved.selectedApplicableScope], - }; - } - return decisionApproachPresetFor(id); + const base = saved + ? { + ...saved, + applicableScope: [...saved.applicableScope], + selectedApplicableScope: [...saved.selectedApplicableScope], + } + : decisionApproachPresetFor(id); + return applyDecisionApproachKeyResources( + base, + selectedKeyResourceLabels, + ); }, - [state.decisionApproachDetailsById], + [selectedKeyResourceLabels, state.decisionApproachDetailsById], ); const handleCardSelect = useCallback( @@ -202,8 +237,31 @@ export function DecisionApproachesScreen() { (next: DecisionApproachDetailEntry) => { markCreateFlowInteraction(); setPendingDraft(next); + const nextCheckedIds = decisionApproachKeyResourceIdsFromLabels( + next.selectedApplicableScope, + ); + if (stringArraysEqual(nextCheckedIds, messageBoxCheckedIds)) { + return; + } + const nextLabels = + selectedKeyResourceLabelsFromCheckedIds(nextCheckedIds); + updateState({ + selectedDecisionKeyResourceIds: nextCheckedIds, + decisionApproachDetailsById: syncDecisionApproachKeyResourceDetails( + state.decisionApproachDetailsById, + selectedIds, + nextLabels, + decisionApproachPresetFor, + ), + }); }, - [markCreateFlowInteraction], + [ + markCreateFlowInteraction, + messageBoxCheckedIds, + selectedIds, + state.decisionApproachDetailsById, + updateState, + ], ); const isSelectedCardModal = @@ -594,7 +652,10 @@ export function DecisionApproachesScreen() { }, decisionApproachDetailsById: { ...(state.decisionApproachDetailsById ?? {}), - [id]: decisionApproachPresetFor(id), + [id]: applyDecisionApproachKeyResources( + decisionApproachPresetFor(id), + selectedKeyResourceLabels, + ), }, customMethodCardFieldBlocksById: { ...(state.customMethodCardFieldBlocksById ?? {}), @@ -606,6 +667,7 @@ export function DecisionApproachesScreen() { markCreateFlowInteraction, pendingDraft, selectedIds, + selectedKeyResourceLabels, state.customMethodCardFieldBlocksById, state.customMethodCardMetaById, state.decisionApproachDetailsById, diff --git a/app/(app)/create/types.ts b/app/(app)/create/types.ts index 02e9071..630ece7 100644 --- a/app/(app)/create/types.ts +++ b/app/(app)/create/types.ts @@ -165,6 +165,12 @@ export interface CreateFlowState { >; membershipMethodDetailsById?: Record; decisionApproachDetailsById?: Record; + /** + * Checked “key resource” ids from the decision-approaches InfoMessageBox + * (`amend`, `finances`, `project`, `discipline`). Selecting one also selects + * the matching Applicable Scope chip on every decision approach. + */ + selectedDecisionKeyResourceIds?: string[]; conflictManagementDetailsById?: Record< string, ConflictManagementDetailEntry diff --git a/app/components/cards/Selection/Selection.view.tsx b/app/components/cards/Selection/Selection.view.tsx index b864a9d..290c900 100644 --- a/app/components/cards/Selection/Selection.view.tsx +++ b/app/components/cards/Selection/Selection.view.tsx @@ -84,14 +84,14 @@ export function SelectionView({ onKeyDown={onKeyDown} >
-
- +
+ {label} {showInfoIcon ? : null}
{supportText ? ( -

+

{supportText}

) : null} diff --git a/app/components/controls/Checkbox/Checkbox.view.tsx b/app/components/controls/Checkbox/Checkbox.view.tsx index f39fc50..c7a9702 100644 --- a/app/components/controls/Checkbox/Checkbox.view.tsx +++ b/app/components/controls/Checkbox/Checkbox.view.tsx @@ -17,7 +17,7 @@ export function CheckboxView({ }: CheckboxViewProps) { return (