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 <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-08-24 16:27:06 -06:00
co-authored by Cursor
parent 38fe0e7e9b
commit b1ca1a748e
19 changed files with 567 additions and 56 deletions
+4 -1
View File
@@ -157,7 +157,10 @@ const decisionRow = {
selectionIds: (s: CreateFlowState) => s.selectedDecisionApproachIds ?? [],
selectedIdsStateKey: "selectedDecisionApproachIds",
detailOverridesStateKey: "decisionApproachDetailsById",
stripSelectionKeys: ["selectedDecisionApproachIds"] as const,
stripSelectionKeys: [
"selectedDecisionApproachIds",
"selectedDecisionKeyResourceIds",
] as const,
apiMethodSectionId: "decisionApproaches",
} satisfies CustomRuleFacetRow;
+149
View File
@@ -0,0 +1,149 @@
import type { DecisionApproachDetailEntry } from "../../app/(app)/create/types";
import decisionApproachesMessages from "../../messages/en/create/customRule/decisionApproaches.json";
export type DecisionApproachKeyResourceItem = {
id: string;
label: string;
};
function uniquePreserveOrder(values: readonly string[]): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const value of values) {
if (seen.has(value)) continue;
seen.add(value);
out.push(value);
}
return out;
}
function isKeyResourceItem(value: unknown): value is DecisionApproachKeyResourceItem {
if (!value || typeof value !== "object") return false;
const row = value as { id?: unknown; label?: unknown };
return typeof row.id === "string" && typeof row.label === "string";
}
/**
* Sidebar “key resource” checkboxes on the decision-approaches step.
* These labels are also offered as Applicable Scope chips on every approach.
*/
export function decisionApproachKeyResourceItems(): DecisionApproachKeyResourceItem[] {
const items = (
decisionApproachesMessages as {
messageBox?: { items?: unknown };
}
).messageBox?.items;
if (!Array.isArray(items)) return [];
return items.filter(isKeyResourceItem);
}
export function decisionApproachKeyResourceLabels(): string[] {
return decisionApproachKeyResourceItems().map((item) => item.label);
}
export function selectedKeyResourceLabelsFromCheckedIds(
checkedIds: readonly string[],
): string[] {
const idSet = new Set(checkedIds);
return decisionApproachKeyResourceItems()
.filter((item) => idSet.has(item.id))
.map((item) => item.label);
}
export function decisionApproachKeyResourceIdsFromLabels(
labels: readonly string[],
): string[] {
const labelSet = new Set(labels);
return decisionApproachKeyResourceItems()
.filter((item) => labelSet.has(item.label))
.map((item) => item.id);
}
export function stringArraysEqual(
a: readonly string[],
b: readonly string[],
): boolean {
if (a.length !== b.length) return false;
return a.every((value, index) => value === b[index]);
}
/**
* Offer the four sidebar labels as Applicable Scope chips without persisting
* them onto `applicableScope` (unchecked keys must not publish as defaults).
*/
export function withDecisionApproachKeyResourceScopes(
scopes: readonly string[],
): string[] {
return uniquePreserveOrder([
...scopes,
...decisionApproachKeyResourceLabels(),
]);
}
/**
* Keep non-key chip selections, then select the key-resource labels whose
* sidebar boxes are checked.
*/
export function applyDecisionApproachKeyResources(
entry: DecisionApproachDetailEntry,
selectedKeyResourceLabels: readonly string[],
): DecisionApproachDetailEntry {
const keyLabels = decisionApproachKeyResourceLabels();
const keySet = new Set(keyLabels);
const selectedSet = new Set(selectedKeyResourceLabels);
return {
...entry,
selectedApplicableScope: uniquePreserveOrder([
...entry.selectedApplicableScope.filter((scope) => !keySet.has(scope)),
...keyLabels.filter((label) => selectedSet.has(label)),
]),
};
}
export function withoutDecisionApproachKeyResourceLabels(
scopes: readonly string[],
): string[] {
const keySet = new Set(decisionApproachKeyResourceLabels());
return scopes.filter((scope) => !keySet.has(scope));
}
/**
* Publish selected original scopes (or all originals when none are picked)
* plus any checked key-resource labels. Checking a key resource must not drop
* unselected default scopes.
*/
export function decisionApproachScopeForPublish(
applicableScope: readonly unknown[],
selectedApplicableScope: readonly unknown[],
): string[] {
const asStrings = (values: readonly unknown[]): string[] =>
values.filter((value): value is string => typeof value === "string");
const keySet = new Set(decisionApproachKeyResourceLabels());
const original = withoutDecisionApproachKeyResourceLabels(
asStrings(applicableScope),
);
const selected = asStrings(selectedApplicableScope);
const selectedOther = withoutDecisionApproachKeyResourceLabels(selected);
const selectedKeys = selected.filter((scope) => keySet.has(scope));
return uniquePreserveOrder([
...(selectedOther.length > 0 ? selectedOther : original),
...selectedKeys,
]);
}
export function syncDecisionApproachKeyResourceDetails(
detailsById: Record<string, DecisionApproachDetailEntry> | undefined,
extraIds: readonly string[],
selectedKeyResourceLabels: readonly string[],
seed: (_id: string) => DecisionApproachDetailEntry,
): Record<string, DecisionApproachDetailEntry> {
const next: Record<string, DecisionApproachDetailEntry> = {};
const ids = new Set<string>([...Object.keys(detailsById ?? {}), ...extraIds]);
for (const id of ids) {
next[id] = applyDecisionApproachKeyResources(
detailsById?.[id] ?? seed(id),
selectedKeyResourceLabels,
);
}
return next;
}
@@ -10,6 +10,7 @@ import {
decisionApproachPresetFor,
membershipPresetFor,
} from "./finalReviewChipPresets";
import { withoutDecisionApproachKeyResourceLabels } from "./decisionApproachKeyResources";
function stringArraysEqual(a: readonly string[], b: readonly string[]): boolean {
if (a.length !== b.length) return false;
@@ -51,8 +52,14 @@ export function decisionApproachFacetMatchesPreset(
const p = decisionApproachPresetFor(cardId);
return (
details.corePrinciple === p.corePrinciple &&
stringArraysEqual(details.applicableScope, p.applicableScope) &&
stringArraysEqual(details.selectedApplicableScope, p.selectedApplicableScope) &&
stringArraysEqual(
withoutDecisionApproachKeyResourceLabels(details.applicableScope),
withoutDecisionApproachKeyResourceLabels(p.applicableScope),
) &&
stringArraysEqual(
withoutDecisionApproachKeyResourceLabels(details.selectedApplicableScope),
withoutDecisionApproachKeyResourceLabels(p.selectedApplicableScope),
) &&
details.stepByStepInstructions === p.stepByStepInstructions &&
details.consensusLevel === p.consensusLevel &&
details.objectionsDeadlocks === p.objectionsDeadlocks
@@ -5,6 +5,7 @@ import type {
} from "../../app/components/type/CommunityRule/CommunityRule.types";
import type { PublishedMethodSelections } from "./buildPublishPayload";
import type { CustomMethodCardFieldBlock } from "./customMethodCardFieldBlocks";
import { decisionApproachScopeForPublish } from "./decisionApproachKeyResources";
import { templateCategoryToGroupKey } from "./templateReviewMapping";
/** Uses filename extension and/or URL path so uploads render as `<img>` vs file link on read-only surfaces. */
@@ -243,9 +244,14 @@ export function sectionFromDecision(
for (const m of ms) {
const sec = m.sections as unknown as Record<string, unknown>;
const merged: Record<string, unknown> = { ...sec };
const scope =
formatScopePayload(sec.selectedApplicableScope) ??
formatScopePayload(sec.applicableScope);
const scope = formatScopePayload(
decisionApproachScopeForPublish(
Array.isArray(sec.applicableScope) ? sec.applicableScope : [],
Array.isArray(sec.selectedApplicableScope)
? sec.selectedApplicableScope
: [],
),
);
if (scope) merged.applicableScope = scope;
delete merged.selectedApplicableScope;
const e = communityRuleEntryFromMethodChip(m.label, merged, DEC_LABELS, {