QA pass: layout, create-flow, and About books #68
@@ -84,6 +84,7 @@ function ApplicableScopeFieldComponent({
|
|||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
onClick={() => !readOnly && onToggleScope(scope)}
|
onClick={() => !readOnly && onToggleScope(scope)}
|
||||||
ariaLabel={`${isSelected ? "Deselect" : "Select"} ${scope}`}
|
ariaLabel={`${isSelected ? "Deselect" : "Select"} ${scope}`}
|
||||||
|
className="max-w-full"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ import {
|
|||||||
decisionApproachPresetFor,
|
decisionApproachPresetFor,
|
||||||
membershipPresetFor,
|
membershipPresetFor,
|
||||||
} from "../../../../lib/create/finalReviewChipPresets";
|
} from "../../../../lib/create/finalReviewChipPresets";
|
||||||
|
import {
|
||||||
|
applyDecisionApproachKeyResources,
|
||||||
|
selectedKeyResourceLabelsFromCheckedIds,
|
||||||
|
} from "../../../../lib/create/decisionApproachKeyResources";
|
||||||
import { isCustomMethodCardId } from "../../../../lib/create/isCustomMethodCardId";
|
import { isCustomMethodCardId } from "../../../../lib/create/isCustomMethodCardId";
|
||||||
import { usesWizardFieldBlocksModalBody } from "../../../../lib/create/usesWizardFieldBlocksModalBody";
|
import { usesWizardFieldBlocksModalBody } from "../../../../lib/create/usesWizardFieldBlocksModalBody";
|
||||||
import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks";
|
import type { CustomMethodCardFieldBlock } from "../../../../lib/create/customMethodCardFieldBlocks";
|
||||||
@@ -1266,6 +1270,7 @@ function facetSeedSignature(
|
|||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
meta: state.customMethodCardMetaById?.[id] ?? null,
|
meta: state.customMethodCardMetaById?.[id] ?? null,
|
||||||
details: state.decisionApproachDetailsById?.[id] ?? null,
|
details: state.decisionApproachDetailsById?.[id] ?? null,
|
||||||
|
keyResources: state.selectedDecisionKeyResourceIds ?? null,
|
||||||
blocks: state.customMethodCardFieldBlocksById?.[id] ?? null,
|
blocks: state.customMethodCardFieldBlocksById?.[id] ?? null,
|
||||||
});
|
});
|
||||||
case "conflictManagement":
|
case "conflictManagement":
|
||||||
@@ -1330,12 +1335,18 @@ function seedDraftForTarget(
|
|||||||
const saved =
|
const saved =
|
||||||
state.decisionApproachDetailsById?.[target.overrideKey] ??
|
state.decisionApproachDetailsById?.[target.overrideKey] ??
|
||||||
decisionApproachPresetFor(target.overrideKey);
|
decisionApproachPresetFor(target.overrideKey);
|
||||||
|
const withKeys = applyDecisionApproachKeyResources(
|
||||||
|
saved,
|
||||||
|
selectedKeyResourceLabelsFromCheckedIds(
|
||||||
|
state.selectedDecisionKeyResourceIds ?? [],
|
||||||
|
),
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
groupKey: "decisionApproaches",
|
groupKey: "decisionApproaches",
|
||||||
value: {
|
value: {
|
||||||
...saved,
|
...withKeys,
|
||||||
applicableScope: [...saved.applicableScope],
|
applicableScope: [...withKeys.applicableScope],
|
||||||
selectedApplicableScope: [...saved.selectedApplicableScope],
|
selectedApplicableScope: [...withKeys.selectedApplicableScope],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
* `markCreateFlowInteraction` live in the parent.
|
* `markCreateFlowInteraction` live in the parent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { memo, useCallback } from "react";
|
import { memo, useCallback, useMemo } from "react";
|
||||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||||
import ModalTextAreaField from "../ModalTextAreaField";
|
import ModalTextAreaField from "../ModalTextAreaField";
|
||||||
import ApplicableScopeField from "../ApplicableScopeField";
|
import ApplicableScopeField from "../ApplicableScopeField";
|
||||||
import IncrementerBlock from "../../../../components/controls/IncrementerBlock";
|
import IncrementerBlock from "../../../../components/controls/IncrementerBlock";
|
||||||
import type { DecisionApproachDetailEntry } from "../../types";
|
import type { DecisionApproachDetailEntry } from "../../types";
|
||||||
|
import { withDecisionApproachKeyResourceScopes } from "../../../../../lib/create/decisionApproachKeyResources";
|
||||||
|
|
||||||
export interface DecisionApproachEditFieldsProps {
|
export interface DecisionApproachEditFieldsProps {
|
||||||
value: DecisionApproachDetailEntry;
|
value: DecisionApproachDetailEntry;
|
||||||
@@ -32,6 +33,11 @@ function DecisionApproachEditFieldsComponent({
|
|||||||
const m = useMessages();
|
const m = useMessages();
|
||||||
const t = m.create.customRule.decisionApproaches;
|
const t = m.create.customRule.decisionApproaches;
|
||||||
|
|
||||||
|
const scopes = useMemo(
|
||||||
|
() => withDecisionApproachKeyResourceScopes(value.applicableScope),
|
||||||
|
[value.applicableScope],
|
||||||
|
);
|
||||||
|
|
||||||
const patch = useCallback(
|
const patch = useCallback(
|
||||||
<K extends keyof DecisionApproachDetailEntry>(
|
<K extends keyof DecisionApproachDetailEntry>(
|
||||||
key: K,
|
key: K,
|
||||||
@@ -53,7 +59,7 @@ function DecisionApproachEditFieldsComponent({
|
|||||||
<ApplicableScopeField
|
<ApplicableScopeField
|
||||||
label={t.sectionHeadings.applicableScope}
|
label={t.sectionHeadings.applicableScope}
|
||||||
addLabel={t.scopeAddButtonLabel}
|
addLabel={t.scopeAddButtonLabel}
|
||||||
scopes={value.applicableScope}
|
scopes={scopes}
|
||||||
selectedScopes={value.selectedApplicableScope}
|
selectedScopes={value.selectedApplicableScope}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onToggleScope={(scope) =>
|
onToggleScope={(scope) =>
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ import { DecisionApproachEditFields } from "../../components/methodEditFields";
|
|||||||
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
import CustomMethodCardWizard from "../../components/CustomMethodCardWizard";
|
||||||
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
import { uploadCreateFlowFile } from "../../../../../lib/create/uploadToServer";
|
||||||
import { decisionApproachPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
|
import { decisionApproachPresetFor } from "../../../../../lib/create/finalReviewChipPresets";
|
||||||
|
import {
|
||||||
|
applyDecisionApproachKeyResources,
|
||||||
|
decisionApproachKeyResourceIdsFromLabels,
|
||||||
|
selectedKeyResourceLabelsFromCheckedIds,
|
||||||
|
stringArraysEqual,
|
||||||
|
syncDecisionApproachKeyResourceDetails,
|
||||||
|
} from "../../../../../lib/create/decisionApproachKeyResources";
|
||||||
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
import type { CustomMethodCardFieldBlock } from "../../../../../lib/create/customMethodCardFieldBlocks";
|
||||||
import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom";
|
import { mergePresetMethodsWithCustom } from "../../../../../lib/create/mergePresetMethodsWithCustom";
|
||||||
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
|
import { moveFacetSelectionIdToFront } from "../../../../../lib/create/methodCardSelectionOrder";
|
||||||
@@ -71,9 +78,6 @@ export function DecisionApproachesScreen() {
|
|||||||
const customizeSnapshotRef = useRef<
|
const customizeSnapshotRef = useRef<
|
||||||
MethodCardCustomizeSnapshot<DecisionApproachDetailEntry> | null
|
MethodCardCustomizeSnapshot<DecisionApproachDetailEntry> | null
|
||||||
>(null);
|
>(null);
|
||||||
const [messageBoxCheckedIds, setMessageBoxCheckedIds] = useState<string[]>(
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
|
const [pendingCardId, setPendingCardId] = useState<string | null>(null);
|
||||||
@@ -90,6 +94,11 @@ export function DecisionApproachesScreen() {
|
|||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const selectedIds = state.selectedDecisionApproachIds ?? [];
|
const selectedIds = state.selectedDecisionApproachIds ?? [];
|
||||||
|
const messageBoxCheckedIds = state.selectedDecisionKeyResourceIds ?? [];
|
||||||
|
const selectedKeyResourceLabels = useMemo(
|
||||||
|
() => selectedKeyResourceLabelsFromCheckedIds(messageBoxCheckedIds),
|
||||||
|
[messageBoxCheckedIds],
|
||||||
|
);
|
||||||
|
|
||||||
const messageBoxItems: InfoMessageBoxItem[] = useMemo(
|
const messageBoxItems: InfoMessageBoxItem[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -137,26 +146,52 @@ export function DecisionApproachesScreen() {
|
|||||||
const handleMessageBoxCheckboxChange = useCallback(
|
const handleMessageBoxCheckboxChange = useCallback(
|
||||||
(id: string, checked: boolean) => {
|
(id: string, checked: boolean) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
setMessageBoxCheckedIds((prev) =>
|
const nextCheckedIds = checked
|
||||||
checked ? [...prev, id] : prev.filter((x) => x !== id),
|
? [...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(
|
const seedDraft = useCallback(
|
||||||
(id: string): DecisionApproachDetailEntry => {
|
(id: string): DecisionApproachDetailEntry => {
|
||||||
const saved = state.decisionApproachDetailsById?.[id];
|
const saved = state.decisionApproachDetailsById?.[id];
|
||||||
if (saved) {
|
const base = saved
|
||||||
return {
|
? {
|
||||||
...saved,
|
...saved,
|
||||||
applicableScope: [...saved.applicableScope],
|
applicableScope: [...saved.applicableScope],
|
||||||
selectedApplicableScope: [...saved.selectedApplicableScope],
|
selectedApplicableScope: [...saved.selectedApplicableScope],
|
||||||
};
|
}
|
||||||
}
|
: decisionApproachPresetFor(id);
|
||||||
return decisionApproachPresetFor(id);
|
return applyDecisionApproachKeyResources(
|
||||||
|
base,
|
||||||
|
selectedKeyResourceLabels,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[state.decisionApproachDetailsById],
|
[selectedKeyResourceLabels, state.decisionApproachDetailsById],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCardSelect = useCallback(
|
const handleCardSelect = useCallback(
|
||||||
@@ -202,8 +237,31 @@ export function DecisionApproachesScreen() {
|
|||||||
(next: DecisionApproachDetailEntry) => {
|
(next: DecisionApproachDetailEntry) => {
|
||||||
markCreateFlowInteraction();
|
markCreateFlowInteraction();
|
||||||
setPendingDraft(next);
|
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 =
|
const isSelectedCardModal =
|
||||||
@@ -594,7 +652,10 @@ export function DecisionApproachesScreen() {
|
|||||||
},
|
},
|
||||||
decisionApproachDetailsById: {
|
decisionApproachDetailsById: {
|
||||||
...(state.decisionApproachDetailsById ?? {}),
|
...(state.decisionApproachDetailsById ?? {}),
|
||||||
[id]: decisionApproachPresetFor(id),
|
[id]: applyDecisionApproachKeyResources(
|
||||||
|
decisionApproachPresetFor(id),
|
||||||
|
selectedKeyResourceLabels,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
customMethodCardFieldBlocksById: {
|
customMethodCardFieldBlocksById: {
|
||||||
...(state.customMethodCardFieldBlocksById ?? {}),
|
...(state.customMethodCardFieldBlocksById ?? {}),
|
||||||
@@ -606,6 +667,7 @@ export function DecisionApproachesScreen() {
|
|||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
pendingDraft,
|
pendingDraft,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
|
selectedKeyResourceLabels,
|
||||||
state.customMethodCardFieldBlocksById,
|
state.customMethodCardFieldBlocksById,
|
||||||
state.customMethodCardMetaById,
|
state.customMethodCardMetaById,
|
||||||
state.decisionApproachDetailsById,
|
state.decisionApproachDetailsById,
|
||||||
|
|||||||
@@ -165,6 +165,12 @@ export interface CreateFlowState {
|
|||||||
>;
|
>;
|
||||||
membershipMethodDetailsById?: Record<string, MembershipMethodDetailEntry>;
|
membershipMethodDetailsById?: Record<string, MembershipMethodDetailEntry>;
|
||||||
decisionApproachDetailsById?: Record<string, DecisionApproachDetailEntry>;
|
decisionApproachDetailsById?: Record<string, DecisionApproachDetailEntry>;
|
||||||
|
/**
|
||||||
|
* 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<
|
conflictManagementDetailsById?: Record<
|
||||||
string,
|
string,
|
||||||
ConflictManagementDetailEntry
|
ConflictManagementDetailEntry
|
||||||
|
|||||||
@@ -84,14 +84,14 @@ export function SelectionView({
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex min-w-0 items-center gap-1">
|
||||||
<span className="text-medium-label text-[var(--color-content-invert-secondary)]">
|
<span className="min-w-0 break-words text-medium-label text-[var(--color-content-invert-secondary)]">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
{showInfoIcon ? <InfoIcon /> : null}
|
{showInfoIcon ? <InfoIcon /> : null}
|
||||||
</div>
|
</div>
|
||||||
{supportText ? (
|
{supportText ? (
|
||||||
<p className="text-x-small-paragraph text-[var(--color-content-invert-tertiary)]">
|
<p className="min-w-0 break-words text-x-small-paragraph text-[var(--color-content-invert-tertiary)]">
|
||||||
{supportText}
|
{supportText}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function CheckboxView({
|
|||||||
}: CheckboxViewProps) {
|
}: CheckboxViewProps) {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={`inline-flex items-center gap-[8px] cursor-pointer select-none ${
|
className={`inline-flex max-w-full min-w-0 items-center gap-[8px] cursor-pointer select-none ${
|
||||||
disabled ? "opacity-60 cursor-not-allowed" : ""
|
disabled ? "opacity-60 cursor-not-allowed" : ""
|
||||||
} ${className}`}
|
} ${className}`}
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
@@ -51,7 +51,7 @@ export function CheckboxView({
|
|||||||
{label && (
|
{label && (
|
||||||
<span
|
<span
|
||||||
id={labelId}
|
id={labelId}
|
||||||
className="text-[14px] leading-[18px]"
|
className="min-w-0 flex-1 text-[14px] leading-[18px] whitespace-normal break-words"
|
||||||
style={{ color: labelColor }}
|
style={{ color: labelColor }}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ function ChipView({
|
|||||||
|
|
||||||
const baseClasses = `
|
const baseClasses = `
|
||||||
inline-flex
|
inline-flex
|
||||||
|
max-w-full
|
||||||
items-center
|
items-center
|
||||||
justify-center
|
justify-center
|
||||||
rounded-[var(--measures-radius-full,9999px)]
|
rounded-[var(--measures-radius-full,9999px)]
|
||||||
@@ -264,7 +265,7 @@ function ChipView({
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...sharedA11y}
|
{...sharedA11y}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center">{label}</span>
|
<span className="min-w-0 truncate">{label}</span>
|
||||||
{onRemove && !isDisabled && (
|
{onRemove && !isDisabled && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -45,30 +45,30 @@ function InfoMessageBoxView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col gap-[12px] p-[var(--spacing-measures-spacing-500,20px)] rounded-[var(--measures-radius-300,12px)] border-l-2 border-solid border-[var(--color-border-default-secondary,#1f1f1f)] bg-[var(--color-content-inverse-secondary,#1f1f1f)] w-full min-w-0 ${className}`}
|
className={`flex w-full min-w-0 items-start gap-[var(--measures-spacing-200,8px)] p-[var(--spacing-measures-spacing-500,20px)] rounded-[var(--measures-radius-300,12px)] border-l-2 border-solid border-[var(--color-border-default-secondary,#1f1f1f)] bg-[var(--color-content-inverse-secondary,#1f1f1f)] ${className}`}
|
||||||
role="region"
|
role="region"
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-[var(--measures-spacing-200,8px)] min-w-0">
|
<div
|
||||||
<div
|
className="relative shrink-0 size-6 flex items-center justify-center"
|
||||||
className="relative shrink-0 size-6 flex items-center justify-center"
|
data-name="Asset / Icon / exclamation"
|
||||||
data-name="Asset / Icon / exclamation"
|
>
|
||||||
>
|
{icon ?? <ExclamationIconInline />}
|
||||||
{icon ?? <ExclamationIconInline />}
|
</div>
|
||||||
</div>
|
<div className="flex min-w-0 flex-1 flex-col gap-[12px]">
|
||||||
<p className="text-small-label text-[var(--color-content-default-primary,white)] min-w-0">
|
<p className="text-small-label text-[var(--color-content-default-primary,white)] min-w-0 break-words">
|
||||||
{title}
|
{title}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<div className="flex min-w-0 w-full flex-col gap-[12px] [&_label]:w-full [&_label]:min-w-0 [&_label]:gap-[6px] [&_label_span]:text-x-small-paragraph [&_label_span]:opacity-80">
|
||||||
<div className="flex flex-col gap-[12px] [&_label]:gap-[6px] [&_label_span]:text-x-small-paragraph [&_label_span]:opacity-80 pl-8">
|
<CheckboxGroup
|
||||||
<CheckboxGroup
|
mode="standard"
|
||||||
mode="standard"
|
value={checkedIds}
|
||||||
value={checkedIds}
|
onChange={handleChange}
|
||||||
onChange={handleChange}
|
options={options}
|
||||||
options={options}
|
aria-label={title}
|
||||||
aria-label={title}
|
className="flex w-full min-w-0 flex-col gap-[12px] !space-y-0"
|
||||||
className="flex flex-col gap-[12px] !space-y-0"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -157,7 +157,10 @@ const decisionRow = {
|
|||||||
selectionIds: (s: CreateFlowState) => s.selectedDecisionApproachIds ?? [],
|
selectionIds: (s: CreateFlowState) => s.selectedDecisionApproachIds ?? [],
|
||||||
selectedIdsStateKey: "selectedDecisionApproachIds",
|
selectedIdsStateKey: "selectedDecisionApproachIds",
|
||||||
detailOverridesStateKey: "decisionApproachDetailsById",
|
detailOverridesStateKey: "decisionApproachDetailsById",
|
||||||
stripSelectionKeys: ["selectedDecisionApproachIds"] as const,
|
stripSelectionKeys: [
|
||||||
|
"selectedDecisionApproachIds",
|
||||||
|
"selectedDecisionKeyResourceIds",
|
||||||
|
] as const,
|
||||||
apiMethodSectionId: "decisionApproaches",
|
apiMethodSectionId: "decisionApproaches",
|
||||||
} satisfies CustomRuleFacetRow;
|
} 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,
|
decisionApproachPresetFor,
|
||||||
membershipPresetFor,
|
membershipPresetFor,
|
||||||
} from "./finalReviewChipPresets";
|
} from "./finalReviewChipPresets";
|
||||||
|
import { withoutDecisionApproachKeyResourceLabels } from "./decisionApproachKeyResources";
|
||||||
|
|
||||||
function stringArraysEqual(a: readonly string[], b: readonly string[]): boolean {
|
function stringArraysEqual(a: readonly string[], b: readonly string[]): boolean {
|
||||||
if (a.length !== b.length) return false;
|
if (a.length !== b.length) return false;
|
||||||
@@ -51,8 +52,14 @@ export function decisionApproachFacetMatchesPreset(
|
|||||||
const p = decisionApproachPresetFor(cardId);
|
const p = decisionApproachPresetFor(cardId);
|
||||||
return (
|
return (
|
||||||
details.corePrinciple === p.corePrinciple &&
|
details.corePrinciple === p.corePrinciple &&
|
||||||
stringArraysEqual(details.applicableScope, p.applicableScope) &&
|
stringArraysEqual(
|
||||||
stringArraysEqual(details.selectedApplicableScope, p.selectedApplicableScope) &&
|
withoutDecisionApproachKeyResourceLabels(details.applicableScope),
|
||||||
|
withoutDecisionApproachKeyResourceLabels(p.applicableScope),
|
||||||
|
) &&
|
||||||
|
stringArraysEqual(
|
||||||
|
withoutDecisionApproachKeyResourceLabels(details.selectedApplicableScope),
|
||||||
|
withoutDecisionApproachKeyResourceLabels(p.selectedApplicableScope),
|
||||||
|
) &&
|
||||||
details.stepByStepInstructions === p.stepByStepInstructions &&
|
details.stepByStepInstructions === p.stepByStepInstructions &&
|
||||||
details.consensusLevel === p.consensusLevel &&
|
details.consensusLevel === p.consensusLevel &&
|
||||||
details.objectionsDeadlocks === p.objectionsDeadlocks
|
details.objectionsDeadlocks === p.objectionsDeadlocks
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
} from "../../app/components/type/CommunityRule/CommunityRule.types";
|
} from "../../app/components/type/CommunityRule/CommunityRule.types";
|
||||||
import type { PublishedMethodSelections } from "./buildPublishPayload";
|
import type { PublishedMethodSelections } from "./buildPublishPayload";
|
||||||
import type { CustomMethodCardFieldBlock } from "./customMethodCardFieldBlocks";
|
import type { CustomMethodCardFieldBlock } from "./customMethodCardFieldBlocks";
|
||||||
|
import { decisionApproachScopeForPublish } from "./decisionApproachKeyResources";
|
||||||
import { templateCategoryToGroupKey } from "./templateReviewMapping";
|
import { templateCategoryToGroupKey } from "./templateReviewMapping";
|
||||||
|
|
||||||
/** Uses filename extension and/or URL path so uploads render as `<img>` vs file link on read-only surfaces. */
|
/** 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) {
|
for (const m of ms) {
|
||||||
const sec = m.sections as unknown as Record<string, unknown>;
|
const sec = m.sections as unknown as Record<string, unknown>;
|
||||||
const merged: Record<string, unknown> = { ...sec };
|
const merged: Record<string, unknown> = { ...sec };
|
||||||
const scope =
|
const scope = formatScopePayload(
|
||||||
formatScopePayload(sec.selectedApplicableScope) ??
|
decisionApproachScopeForPublish(
|
||||||
formatScopePayload(sec.applicableScope);
|
Array.isArray(sec.applicableScope) ? sec.applicableScope : [],
|
||||||
|
Array.isArray(sec.selectedApplicableScope)
|
||||||
|
? sec.selectedApplicableScope
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
if (scope) merged.applicableScope = scope;
|
if (scope) merged.applicableScope = scope;
|
||||||
delete merged.selectedApplicableScope;
|
delete merged.selectedApplicableScope;
|
||||||
const e = communityRuleEntryFromMethodChip(m.label, merged, DEC_LABELS, {
|
const e = communityRuleEntryFromMethodChip(m.label, merged, DEC_LABELS, {
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export const createFlowStateSchema = z
|
|||||||
selectedCommunicationMethodIds: z.array(z.string()).max(200).optional(),
|
selectedCommunicationMethodIds: z.array(z.string()).max(200).optional(),
|
||||||
selectedMembershipMethodIds: z.array(z.string()).max(200).optional(),
|
selectedMembershipMethodIds: z.array(z.string()).max(200).optional(),
|
||||||
selectedDecisionApproachIds: 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(),
|
selectedConflictManagementIds: z.array(z.string()).max(200).optional(),
|
||||||
communicationMethodDetailsById: z
|
communicationMethodDetailsById: z
|
||||||
.record(communicationMethodDetailEntrySchema)
|
.record(communicationMethodDetailEntrySchema)
|
||||||
|
|||||||
@@ -34,4 +34,11 @@ describe("InfoMessageBox", () => {
|
|||||||
await u.click(checkbox);
|
await u.click(checkbox);
|
||||||
expect(onCheckboxChange).toHaveBeenCalled();
|
expect(onCheckboxChange).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the checkbox column shrinkable so labels wrap", () => {
|
||||||
|
render(<InfoMessageBox title="Important" items={items} />);
|
||||||
|
const region = screen.getByRole("region", { name: "Important" });
|
||||||
|
expect(region).toHaveClass("min-w-0");
|
||||||
|
expect(region).toHaveClass("w-full");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -272,4 +272,91 @@ describe("Create flow decision-approaches page", () => {
|
|||||||
await user.click(amendCheckbox);
|
await user.click(amendCheckbox);
|
||||||
expect(amendCheckbox).toBeChecked();
|
expect(amendCheckbox).toBeChecked();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("each approach modal includes the key-resource applicable-scope chips", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DecisionApproachesScreen />);
|
||||||
|
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole("button", {
|
||||||
|
name: /Lazy Consensus: A decision is assumed approved/,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
expect(
|
||||||
|
within(dialog).getByRole("button", {
|
||||||
|
name: "Select Amend your CommunityRule",
|
||||||
|
}),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
within(dialog).getByRole("button", { name: "Select Steward finances" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
within(dialog).getByRole("button", {
|
||||||
|
name: "Select Project level decisions",
|
||||||
|
}),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
within(dialog).getByRole("button", {
|
||||||
|
name: "Select Discipline and member termination",
|
||||||
|
}),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("checking a key-resource box selects that chip on every approach", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DecisionApproachesScreen />);
|
||||||
|
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole("checkbox", { name: "Steward finances" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole("button", {
|
||||||
|
name: /Lazy Consensus: A decision is assumed approved/,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const lazyDialog = await screen.findByRole("dialog");
|
||||||
|
expect(
|
||||||
|
within(lazyDialog).getByRole("button", {
|
||||||
|
name: "Deselect Steward finances",
|
||||||
|
}),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
await user.click(within(lazyDialog).getByRole("button", { name: "Close dialog" }));
|
||||||
|
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole("button", {
|
||||||
|
name: /Do-ocracy: Decisions are made by those who take initiative/,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const doocracyDialog = await screen.findByRole("dialog");
|
||||||
|
expect(
|
||||||
|
within(doocracyDialog).getByRole("button", {
|
||||||
|
name: "Deselect Steward finances",
|
||||||
|
}),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("choosing a key-resource chip checks the matching sidebar box", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<DecisionApproachesScreen />);
|
||||||
|
|
||||||
|
await user.click(
|
||||||
|
screen.getByRole("button", {
|
||||||
|
name: /Lazy Consensus: A decision is assumed approved/,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
await user.click(
|
||||||
|
within(dialog).getByRole("button", {
|
||||||
|
name: "Select Discipline and member termination",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole("checkbox", {
|
||||||
|
name: "Discipline and member termination",
|
||||||
|
}),
|
||||||
|
).toBeChecked();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
applyDecisionApproachKeyResources,
|
||||||
|
decisionApproachKeyResourceIdsFromLabels,
|
||||||
|
decisionApproachScopeForPublish,
|
||||||
|
decisionApproachKeyResourceItems,
|
||||||
|
decisionApproachKeyResourceLabels,
|
||||||
|
selectedKeyResourceLabelsFromCheckedIds,
|
||||||
|
stringArraysEqual,
|
||||||
|
syncDecisionApproachKeyResourceDetails,
|
||||||
|
withDecisionApproachKeyResourceScopes,
|
||||||
|
} from "../../lib/create/decisionApproachKeyResources";
|
||||||
|
import type { DecisionApproachDetailEntry } from "../../app/(app)/create/types";
|
||||||
|
|
||||||
|
const emptyEntry = (): DecisionApproachDetailEntry => ({
|
||||||
|
corePrinciple: "p",
|
||||||
|
applicableScope: ["Daily Operations"],
|
||||||
|
selectedApplicableScope: [],
|
||||||
|
stepByStepInstructions: "s",
|
||||||
|
consensusLevel: 75,
|
||||||
|
objectionsDeadlocks: "o",
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decisionApproachKeyResources", () => {
|
||||||
|
it("reads the four key-resource items from messages", () => {
|
||||||
|
expect(decisionApproachKeyResourceItems()).toEqual([
|
||||||
|
{ id: "amend", label: "Amend your CommunityRule" },
|
||||||
|
{ id: "finances", label: "Steward finances" },
|
||||||
|
{ id: "project", label: "Project level decisions" },
|
||||||
|
{ id: "discipline", label: "Discipline and member termination" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selects checked key-resource labels without rewriting applicableScope", () => {
|
||||||
|
const next = applyDecisionApproachKeyResources(emptyEntry(), [
|
||||||
|
"Steward finances",
|
||||||
|
]);
|
||||||
|
expect(next.applicableScope).toEqual(["Daily Operations"]);
|
||||||
|
expect(next.selectedApplicableScope).toEqual(["Steward finances"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps non-key chip selections when syncing sidebar checks", () => {
|
||||||
|
const next = applyDecisionApproachKeyResources(
|
||||||
|
{
|
||||||
|
...emptyEntry(),
|
||||||
|
applicableScope: ["Steward finances", "Daily Operations"],
|
||||||
|
selectedApplicableScope: ["Daily Operations"],
|
||||||
|
},
|
||||||
|
["Steward finances"],
|
||||||
|
);
|
||||||
|
expect(next.applicableScope).toEqual([
|
||||||
|
"Steward finances",
|
||||||
|
"Daily Operations",
|
||||||
|
]);
|
||||||
|
expect(next.selectedApplicableScope).toEqual([
|
||||||
|
"Daily Operations",
|
||||||
|
"Steward finances",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("publishes default scopes plus checked key resources", () => {
|
||||||
|
expect(
|
||||||
|
decisionApproachScopeForPublish(
|
||||||
|
["Daily Operations", "Minor Expenditures"],
|
||||||
|
["Steward finances"],
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
"Daily Operations",
|
||||||
|
"Minor Expenditures",
|
||||||
|
"Steward finances",
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
decisionApproachScopeForPublish(
|
||||||
|
["Daily Operations", "Minor Expenditures"],
|
||||||
|
["Daily Operations", "Steward finances"],
|
||||||
|
),
|
||||||
|
).toEqual(["Daily Operations", "Steward finances"]);
|
||||||
|
expect(
|
||||||
|
decisionApproachScopeForPublish(
|
||||||
|
["Daily Operations", "Minor Expenditures"],
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
).toEqual(["Daily Operations", "Minor Expenditures"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps checkbox ids to labels and back", () => {
|
||||||
|
expect(selectedKeyResourceLabelsFromCheckedIds(["amend", "project"])).toEqual(
|
||||||
|
["Amend your CommunityRule", "Project level decisions"],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
decisionApproachKeyResourceIdsFromLabels([
|
||||||
|
"Project level decisions",
|
||||||
|
"Unknown",
|
||||||
|
"Amend your CommunityRule",
|
||||||
|
]),
|
||||||
|
).toEqual(["amend", "project"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("syncs existing and selected approach details", () => {
|
||||||
|
const next = syncDecisionApproachKeyResourceDetails(
|
||||||
|
{ "lazy-consensus": emptyEntry() },
|
||||||
|
["do-ocracy"],
|
||||||
|
["Amend your CommunityRule"],
|
||||||
|
(id) => ({
|
||||||
|
...emptyEntry(),
|
||||||
|
corePrinciple: id,
|
||||||
|
applicableScope: ["Volunteer Tasks"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(next["lazy-consensus"]?.selectedApplicableScope).toEqual([
|
||||||
|
"Amend your CommunityRule",
|
||||||
|
]);
|
||||||
|
expect(next["do-ocracy"]?.corePrinciple).toBe("do-ocracy");
|
||||||
|
expect(next["do-ocracy"]?.applicableScope).toEqual(["Volunteer Tasks"]);
|
||||||
|
expect(next["do-ocracy"]?.selectedApplicableScope).toEqual([
|
||||||
|
"Amend your CommunityRule",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("compares string arrays by order", () => {
|
||||||
|
expect(stringArraysEqual(["a", "b"], ["a", "b"])).toBe(true);
|
||||||
|
expect(stringArraysEqual(["a", "b"], ["b", "a"])).toBe(false);
|
||||||
|
expect(decisionApproachKeyResourceLabels()).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("unions key-resource labels onto an existing scope list", () => {
|
||||||
|
expect(
|
||||||
|
withDecisionApproachKeyResourceScopes(["Daily Operations"]),
|
||||||
|
).toEqual([
|
||||||
|
"Daily Operations",
|
||||||
|
"Amend your CommunityRule",
|
||||||
|
"Steward finances",
|
||||||
|
"Project level decisions",
|
||||||
|
"Discipline and member termination",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { communicationPresetFor } from "../../lib/create/finalReviewChipPresets";
|
import {
|
||||||
import { communicationMethodFacetMatchesPreset } from "../../lib/create/methodCardFacetMatchesPresetForId";
|
communicationPresetFor,
|
||||||
|
decisionApproachPresetFor,
|
||||||
|
} from "../../lib/create/finalReviewChipPresets";
|
||||||
|
import {
|
||||||
|
communicationMethodFacetMatchesPreset,
|
||||||
|
decisionApproachFacetMatchesPreset,
|
||||||
|
} from "../../lib/create/methodCardFacetMatchesPresetForId";
|
||||||
|
|
||||||
const uuid = "550e8400-e29b-41d4-a716-446655440000";
|
const uuid = "550e8400-e29b-41d4-a716-446655440000";
|
||||||
|
|
||||||
@@ -19,4 +25,23 @@ describe("methodCardFacetMatchesPresetForId", () => {
|
|||||||
),
|
),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("decision approaches: ignores key-resource chip selections", () => {
|
||||||
|
const p = decisionApproachPresetFor(uuid);
|
||||||
|
expect(
|
||||||
|
decisionApproachFacetMatchesPreset(
|
||||||
|
{
|
||||||
|
...p,
|
||||||
|
selectedApplicableScope: ["Steward finances"],
|
||||||
|
},
|
||||||
|
uuid,
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
decisionApproachFacetMatchesPreset(
|
||||||
|
{ ...p, corePrinciple: "edited" },
|
||||||
|
uuid,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ describe("stripCustomRuleSelectionFields", () => {
|
|||||||
selectedCommunicationMethodIds: ["signal"],
|
selectedCommunicationMethodIds: ["signal"],
|
||||||
selectedMembershipMethodIds: ["x"],
|
selectedMembershipMethodIds: ["x"],
|
||||||
selectedDecisionApproachIds: ["y"],
|
selectedDecisionApproachIds: ["y"],
|
||||||
|
selectedDecisionKeyResourceIds: ["amend"],
|
||||||
selectedConflictManagementIds: ["z"],
|
selectedConflictManagementIds: ["z"],
|
||||||
methodSectionsPinCommitted: { communication: true },
|
methodSectionsPinCommitted: { communication: true },
|
||||||
coreValueDetailsByChipId: { "1": { meaning: "", signals: "" } },
|
coreValueDetailsByChipId: { "1": { meaning: "", signals: "" } },
|
||||||
@@ -38,6 +39,7 @@ describe("stripCustomRuleSelectionFields", () => {
|
|||||||
expect(out.selectedCommunicationMethodIds).toBeUndefined();
|
expect(out.selectedCommunicationMethodIds).toBeUndefined();
|
||||||
expect(out.selectedMembershipMethodIds).toBeUndefined();
|
expect(out.selectedMembershipMethodIds).toBeUndefined();
|
||||||
expect(out.selectedDecisionApproachIds).toBeUndefined();
|
expect(out.selectedDecisionApproachIds).toBeUndefined();
|
||||||
|
expect(out.selectedDecisionKeyResourceIds).toBeUndefined();
|
||||||
expect(out.selectedConflictManagementIds).toBeUndefined();
|
expect(out.selectedConflictManagementIds).toBeUndefined();
|
||||||
expect(out.methodSectionsPinCommitted).toBeUndefined();
|
expect(out.methodSectionsPinCommitted).toBeUndefined();
|
||||||
expect(out.coreValueDetailsByChipId).toBeUndefined();
|
expect(out.coreValueDetailsByChipId).toBeUndefined();
|
||||||
|
|||||||
Reference in New Issue
Block a user