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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -117,6 +117,7 @@ export const createFlowStateSchema = z
|
||||
selectedCommunicationMethodIds: z.array(z.string()).max(200).optional(),
|
||||
selectedMembershipMethodIds: z.array(z.string()).max(200).optional(),
|
||||
selectedDecisionApproachIds: z.array(z.string()).max(200).optional(),
|
||||
selectedDecisionKeyResourceIds: z.array(z.string().max(80)).max(20).optional(),
|
||||
selectedConflictManagementIds: z.array(z.string()).max(200).optional(),
|
||||
communicationMethodDetailsById: z
|
||||
.record(communicationMethodDetailEntrySchema)
|
||||
|
||||
Reference in New Issue
Block a user