Merge pull request 'Ask the six optional setup questions after size so each tick can change suggested cards' (#97) from adilallo/fix/CR-294-setup-questions into main
Reviewed-on: #97
This commit was merged in pull request #97.
This commit is contained in:
commit
2a7b9e6da7
25 files changed
+442
-86
No files matched your search
@@ -91,6 +91,10 @@ import {
|
||||
isCommunitySizeStepBlocked,
|
||||
isCommunityStructureStepBlocked,
|
||||
} from "./utils/createFlowFooterLabels";
|
||||
import {
|
||||
EMPTY_SETUP_TRAITS,
|
||||
hasTickedSetupTrait,
|
||||
} from "../../../lib/create/setupTraits";
|
||||
import { useAuthModal } from "../../contexts/AuthModalContext";
|
||||
import { useMessages, useTranslation } from "../../contexts/MessagesContext";
|
||||
import { PostLoginDraftTransfer } from "./PostLoginDraftTransfer";
|
||||
@@ -984,6 +988,38 @@ function CreateFlowLayoutContent({
|
||||
: footer.submitEmail}
|
||||
</Button>
|
||||
</div>
|
||||
) : currentStep === "setup-questions" && nextStep ? (
|
||||
<div className="flex flex-shrink-0 items-center gap-3 md:gap-4">
|
||||
<Button
|
||||
buttonType="filled"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={
|
||||
isPublishing || !hasTickedSetupTrait(state.setupTraits)
|
||||
}
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||
onClick={() => {
|
||||
markCreateFlowInteraction();
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{footer.continue}
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="outline"
|
||||
palette="default"
|
||||
size="xsmall"
|
||||
disabled={isPublishing}
|
||||
className={CREATE_FLOW_FOOTER_BUTTON_CLASS}
|
||||
onClick={() => {
|
||||
markCreateFlowInteraction();
|
||||
updateState({ setupTraits: { ...EMPTY_SETUP_TRAITS } });
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{footer.skip}
|
||||
</Button>
|
||||
</div>
|
||||
) : currentStep === "community-upload" && nextStep ? (
|
||||
<Button
|
||||
buttonType={hasCommunityLogo ? "filled" : "outline"}
|
||||
|
||||
@@ -30,7 +30,9 @@ export type UseTemplateReviewActionsResult = {
|
||||
* Customize: apply the template's selections onto state and route to
|
||||
* `/create/core-values` (if community name is set) or `/create/informational`
|
||||
* with a `pendingTemplateAction` pin so `/create/review` can later replace
|
||||
* itself with `/create/core-values`.
|
||||
* itself with `/create/core-values`. A named community skips setup
|
||||
* questions, so leftover setup answers are dropped and cannot change
|
||||
* suggested cards.
|
||||
*/
|
||||
handleCustomize: () => Promise<void>;
|
||||
/**
|
||||
@@ -39,8 +41,9 @@ export type UseTemplateReviewActionsResult = {
|
||||
* Customize) so drilling from final-review via + shows selected cards, drop
|
||||
* the Values row from `state.sections`. Direct template entry routes to
|
||||
* `/create/community-name` with a pin so Next/Back collect only name →
|
||||
* description → photo. In-flow (`?fromFlow=1`, community steps already done)
|
||||
* routes to `/create/confirm-stakeholders`.
|
||||
* description → photo, and drops leftover setup answers along with the
|
||||
* other identity fields. In-flow (`?fromFlow=1`, community steps already
|
||||
* done) routes to `/create/confirm-stakeholders` and keeps those answers.
|
||||
*/
|
||||
handleUseWithoutChanges: () => Promise<void>;
|
||||
};
|
||||
@@ -128,7 +131,7 @@ export function useTemplateReviewActions({
|
||||
},
|
||||
templateReviewBackSlug: undefined,
|
||||
...(hasCommunityName
|
||||
? { pendingTemplateAction: undefined }
|
||||
? { pendingTemplateAction: undefined, setupTraits: undefined }
|
||||
: {
|
||||
pendingTemplateAction: {
|
||||
slug: templateReviewSlug,
|
||||
@@ -256,6 +259,7 @@ export function useTemplateReviewActions({
|
||||
communityAvatarUrl: undefined,
|
||||
currentStep: undefined,
|
||||
templateReviewEntryFromCreateFlow: undefined,
|
||||
setupTraits: undefined,
|
||||
pendingTemplateAction: {
|
||||
slug: templateReviewSlug,
|
||||
mode: "useWithoutChanges",
|
||||
|
||||
@@ -27,6 +27,13 @@ const CommunitySizeSelectScreen = dynamic(
|
||||
})),
|
||||
{ loading: () => null },
|
||||
);
|
||||
const SetupQuestionsScreen = dynamic(
|
||||
() =>
|
||||
import("./select/SetupQuestionsScreen").then((m) => ({
|
||||
default: m.SetupQuestionsScreen,
|
||||
})),
|
||||
{ loading: () => null },
|
||||
);
|
||||
const CommunityStructureSelectScreen = dynamic(
|
||||
() =>
|
||||
import("./select/CommunityStructureSelectScreen").then((m) => ({
|
||||
@@ -135,6 +142,8 @@ export function renderCreateFlowScreen(screenId: CreateFlowStep): ReactNode {
|
||||
);
|
||||
case "community-size":
|
||||
return <CommunitySizeSelectScreen />;
|
||||
case "setup-questions":
|
||||
return <SetupQuestionsScreen />;
|
||||
case "community-upload":
|
||||
return <CommunityUploadScreen />;
|
||||
case "community-save":
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import CheckboxGroup from "../../../../components/controls/CheckboxGroup";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||
import {
|
||||
setupTraitsFromTickedIds,
|
||||
tickedSetupTraitIds,
|
||||
} from "../../../../../lib/create/setupTraits";
|
||||
|
||||
/**
|
||||
* Create Community — Figma Flow - Checkboxes (25224:105566).
|
||||
*/
|
||||
export function SetupQuestionsScreen() {
|
||||
const copy = useMessages().create.community.setupQuestions;
|
||||
const { markCreateFlowInteraction, updateState, state } = useCreateFlow();
|
||||
|
||||
return (
|
||||
<CreateFlowTwoColumnSelectShell
|
||||
header={
|
||||
<CreateFlowHeaderLockup
|
||||
title={copy.title}
|
||||
description={copy.description}
|
||||
justification="left"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CheckboxGroup
|
||||
value={tickedSetupTraitIds(state.setupTraits)}
|
||||
onChange={({ value }) => {
|
||||
markCreateFlowInteraction();
|
||||
updateState({ setupTraits: setupTraitsFromTickedIds(value) });
|
||||
}}
|
||||
options={copy.questions.map((question) => ({
|
||||
value: question.id,
|
||||
label: question.label,
|
||||
subtext: question.help,
|
||||
}))}
|
||||
aria-label={copy.title}
|
||||
className="flex w-full flex-col gap-[20px] !space-y-0"
|
||||
/>
|
||||
</CreateFlowTwoColumnSelectShell>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export type CreateFlowStep =
|
||||
| "informational"
|
||||
| "community-name"
|
||||
| "community-size"
|
||||
| "setup-questions"
|
||||
| "community-context"
|
||||
| "community-structure"
|
||||
| "community-upload"
|
||||
|
||||
@@ -12,7 +12,8 @@ const PROPORTION_BY_STEP_INDEX: readonly ProportionBarState[] = [
|
||||
"1-2", // community-structure
|
||||
"1-3", // community-context
|
||||
"1-4", // community-size
|
||||
"1-6", // community-upload (1-5 is reserved for setup questions)
|
||||
"1-5", // setup-questions
|
||||
"1-6", // community-upload
|
||||
"1-7", // community-save
|
||||
"1-8", // review
|
||||
"2-0", // core-values (same fill as 1-8)
|
||||
|
||||
@@ -57,6 +57,12 @@ export const CREATE_FLOW_SCREEN_REGISTRY: Record<
|
||||
messageNamespace: "create.community.communitySize",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"setup-questions": {
|
||||
layoutKind: "select",
|
||||
figmaNodeId: "25224-105566",
|
||||
messageNamespace: "create.community.setupQuestions",
|
||||
centeredBodyBelowMd: false,
|
||||
},
|
||||
"community-context": {
|
||||
layoutKind: "text",
|
||||
figmaNodeId: "25224-106030",
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
* Step definitions and helpers for the Create Rule Flow
|
||||
*
|
||||
* Single source of truth for step order and navigation helpers.
|
||||
* Order matches Figma Create Community (frames 1–8) then later stages.
|
||||
* Order matches Figma Create Community then later stages.
|
||||
* `community-structure` precedes `community-context` and `community-size` (Figma frame 3 vs 5 swap).
|
||||
* `setup-questions` sits after size and before the logo upload.
|
||||
*/
|
||||
|
||||
import type { CreateFlowStep } from "../types";
|
||||
@@ -17,6 +18,7 @@ export const FLOW_STEP_ORDER: readonly CreateFlowStep[] = [
|
||||
"community-structure",
|
||||
"community-context",
|
||||
"community-size",
|
||||
"setup-questions",
|
||||
"community-upload",
|
||||
"community-save",
|
||||
"review",
|
||||
@@ -49,8 +51,8 @@ export type CreateFlowNavigationOptions = {
|
||||
skipCommunitySave?: boolean;
|
||||
/**
|
||||
* Template **Use without changes**: collect community name, description, and
|
||||
* optional photo only. Skips intro, structure, size, save, community review,
|
||||
* and the custom-rule authoring segment.
|
||||
* optional photo only. Skips intro, structure, size, setup questions, save,
|
||||
* community review, and the custom-rule authoring segment.
|
||||
*/
|
||||
useWithoutChangesIdentityOnly?: boolean;
|
||||
};
|
||||
@@ -65,6 +67,7 @@ const USE_WITHOUT_CHANGES_IDENTITY_SKIP = new Set<CreateFlowStep>([
|
||||
"informational",
|
||||
"community-structure",
|
||||
"community-size",
|
||||
"setup-questions",
|
||||
"community-save",
|
||||
"review",
|
||||
"core-values",
|
||||
@@ -199,6 +202,7 @@ export function shouldOfferCreateFlowSaveAndExit(
|
||||
*/
|
||||
export const CREATE_FLOW_SELECT_SPLIT_SCROLL_STEPS: readonly CreateFlowStep[] = [
|
||||
"community-size",
|
||||
"setup-questions",
|
||||
"community-structure",
|
||||
"core-values",
|
||||
"communication-methods",
|
||||
|
||||
@@ -35,9 +35,14 @@ export function CheckboxGroupView({
|
||||
onOptionChange(option.value, checked);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-[4px] flex-1">
|
||||
<div
|
||||
className="flex flex-col gap-[4px] flex-1 min-w-0 cursor-pointer"
|
||||
onClick={() => {
|
||||
if (!disabled) onOptionChange(option.value, !isChecked);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`text-small-paragraph ${
|
||||
className={`text-paragraph-m ${
|
||||
mode === "inverse"
|
||||
? "text-[var(--color-content-invert-primary)]"
|
||||
: "text-[var(--color-content-default-primary)]"
|
||||
|
||||
+15
-14
@@ -6,11 +6,11 @@ Product/engineering reference for the **custom** “Create rule” experience: U
|
||||
|
||||
## Product stages (Figma)
|
||||
|
||||
The Figma **Create Community** sequence is the **source of truth** for the first segment of the wizard (eight frames). After **`review`**, the flow continues with **Create Custom CommunityRule** and **Review and complete** stages. The shipped URL sequence in [`FLOW_STEP_ORDER`](../app/(app)/create/utils/flowSteps.ts) **follows that trajectory**; stages are a **product** slice of that linear order, not separate routers today.
|
||||
The Figma **Create Community** sequence is the **source of truth** for the first segment of the wizard. After **`review`**, the flow continues with **Create Custom CommunityRule** and **Review and complete** stages. The shipped URL sequence in [`FLOW_STEP_ORDER`](../app/(app)/create/utils/flowSteps.ts) **follows that trajectory**; stages are a **product** slice of that linear order, not separate routers today.
|
||||
|
||||
| Stage (Figma) | Purpose (summary) | `CreateFlowStep` values (in order) |
|
||||
| --- | --- | --- |
|
||||
| **Create Community** | Intro, naming, structure, context, size, upload, save progress (email), then community review. | `informational` → `community-name` → `community-structure` → `community-context` → `community-size` → `community-upload` → `community-save` → `review` |
|
||||
| **Create Community** | Intro, naming, structure, context, size, optional setup questions, upload, save progress (email), then community review. | `informational` → `community-name` → `community-structure` → `community-context` → `community-size` → `setup-questions` → `community-upload` → `community-save` → `review` |
|
||||
| **Create Custom CommunityRule** | Author the CommunityRule content and structure (core values + four card-stack categories). | `core-values` → `communication-methods` → `membership-methods` → `decision-approaches` → `conflict-management` |
|
||||
| **Review and complete** | Stakeholders, final card, publish, success. | `confirm-stakeholders` → `final-review` → `completed` |
|
||||
|
||||
@@ -31,17 +31,18 @@ Order is defined in code by [`FLOW_STEP_ORDER`](../app/(app)/create/utils/flowSt
|
||||
| 3 | Create Community | `community-structure` | `/create/community-structure` |
|
||||
| 4 | Create Community | `community-context` | `/create/community-context` |
|
||||
| 5 | Create Community | `community-size` | `/create/community-size` |
|
||||
| 6 | Create Community | `community-upload` | `/create/community-upload` |
|
||||
| 7 | Create Community | `community-save` | `/create/community-save` |
|
||||
| 8 | Create Community (review frame) | `review` | `/create/review` |
|
||||
| 9 | Create Custom CommunityRule | `core-values` | `/create/core-values` |
|
||||
| 10 | Create Custom CommunityRule | `communication-methods` | `/create/communication-methods` |
|
||||
| 11 | Create Custom CommunityRule | `membership-methods` | `/create/membership-methods` |
|
||||
| 12 | Create Custom CommunityRule | `decision-approaches` | `/create/decision-approaches` |
|
||||
| 13 | Create Custom CommunityRule | `conflict-management` | `/create/conflict-management` |
|
||||
| 14 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` |
|
||||
| 15 | Review and complete | `final-review` | `/create/final-review` |
|
||||
| 16 | Review and complete | `completed` | `/create/completed` |
|
||||
| 6 | Create Community | `setup-questions` | `/create/setup-questions` |
|
||||
| 7 | Create Community | `community-upload` | `/create/community-upload` |
|
||||
| 8 | Create Community | `community-save` | `/create/community-save` |
|
||||
| 9 | Create Community (review frame) | `review` | `/create/review` |
|
||||
| 10 | Create Custom CommunityRule | `core-values` | `/create/core-values` |
|
||||
| 11 | Create Custom CommunityRule | `communication-methods` | `/create/communication-methods` |
|
||||
| 12 | Create Custom CommunityRule | `membership-methods` | `/create/membership-methods` |
|
||||
| 13 | Create Custom CommunityRule | `decision-approaches` | `/create/decision-approaches` |
|
||||
| 14 | Create Custom CommunityRule | `conflict-management` | `/create/conflict-management` |
|
||||
| 15 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` |
|
||||
| 16 | Review and complete | `final-review` | `/create/final-review` |
|
||||
| 17 | Review and complete | `completed` | `/create/completed` |
|
||||
|
||||
**Primary entry:** marketing header **Create rule** and profile **Create new custom Rule** both run **`prepareFreshCreateFlowEntry`** then navigate to **`/create`**, which redirects to **`/create/informational`** (see [`Top.container.tsx`](../app/components/navigation/Top/Top.container.tsx) and [`ProfilePageClient.tsx`](../app/(app)/profile/ProfilePageClient.tsx)).
|
||||
|
||||
@@ -87,7 +88,7 @@ From that page, **Customize** pre-fills the custom-rule selections on the curren
|
||||
**Entering a template before community stage is done.** Both handlers apply their side effects eagerly (prefill for Customize; `sections` for Use without changes) *and* pin a `pendingTemplateAction: { slug, mode }` when the community questionnaire still needs to run.
|
||||
|
||||
- **Customize** routes to `/create/informational` and walks the full Create Community questionnaire. Once the user reaches `/create/review`, [`CommunityReviewScreen`](../app/(app)/create/screens/review/CommunityReviewScreen.tsx) reads the action on mount, clears it via `updateState`, and `router.replace`s to `/create/core-values`. `replace` (not `push`) keeps `community-save` as the Back-button target from that destination.
|
||||
- **Use without changes (direct from a template)** — home Popular templates, `/templates` without `fromFlow`, or a pasted review-template URL. Always routes to `/create/community-name` and walks **name → description → optional photo**, then `/create/confirm-stakeholders`. A leftover draft title is **not** “community already done”; those identity fields are cleared so cache cannot skip or pre-fill the path. Intro, structure, size, save, community review, and custom-rule authoring are skipped. Back from name returns to template review.
|
||||
- **Use without changes (direct from a template)** — home Popular templates, `/templates` without `fromFlow`, or a pasted review-template URL. Always routes to `/create/community-name` and walks **name → description → optional photo**, then `/create/confirm-stakeholders`. A leftover draft title is **not** “community already done”; those identity fields are cleared so cache cannot skip or pre-fill the path. Intro, structure, size, setup questions, save, community review, and custom-rule authoring are skipped. Back from name returns to template review.
|
||||
- **Use without changes (in-flow)** — `/create/review` → `/create/templates` → template. Community identity is already collected, so this action seeds the template and routes to `/create/confirm-stakeholders`. Back returns to template review.
|
||||
|
||||
The action for Customize is cleared on the first fire so later direct visits to `/create/review` render normally. The review screen is not part of the Use-without-changes identity path. If they land on `/create/review` with a leftover Use-without-changes pin, the screen `replace`s to `community-name`.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { CreateFlowState } from "../../app/(app)/create/types";
|
||||
import { TRAIT_KEYS, type TraitKey } from "./deckTypes";
|
||||
|
||||
export const EMPTY_SETUP_TRAITS: Record<TraitKey, boolean> = {
|
||||
network: false,
|
||||
nonvoters: false,
|
||||
employer: false,
|
||||
sponsor: false,
|
||||
records: false,
|
||||
risky: false,
|
||||
};
|
||||
|
||||
export function tickedSetupTraitIds(
|
||||
traits: CreateFlowState["setupTraits"],
|
||||
): TraitKey[] {
|
||||
if (!traits) return [];
|
||||
return TRAIT_KEYS.filter((key) => traits[key] === true);
|
||||
}
|
||||
|
||||
export function setupTraitsFromTickedIds(
|
||||
ids: readonly string[],
|
||||
): Record<TraitKey, boolean> {
|
||||
const ticked = new Set(ids);
|
||||
const out = { ...EMPTY_SETUP_TRAITS };
|
||||
for (const key of TRAIT_KEYS) {
|
||||
out[key] = ticked.has(key);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function hasTickedSetupTrait(
|
||||
traits: CreateFlowState["setupTraits"],
|
||||
): boolean {
|
||||
return tickedSetupTraitIds(traits).length > 0;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"_comment": "Create Community setup questions (Figma Flow - Checkboxes 25224:105566). Copy from Figma at build time. Question ids are the trait keys the decks read.",
|
||||
"title": "A few more things about your community",
|
||||
"description": "Tick any that are true. Each one adds or changes a suggested card. We keep your answers with your draft and never publish them.",
|
||||
"questions": [
|
||||
{
|
||||
"id": "network",
|
||||
"label": "Some of our members or partners are organizations",
|
||||
"help": "Such as crews in a network, organizations in a coalition or businesses in a co-op"
|
||||
},
|
||||
{
|
||||
"id": "nonvoters",
|
||||
"label": "Some people who do our work do not vote",
|
||||
"help": "Such as volunteers who are not members, staff who are not owners or contractors"
|
||||
},
|
||||
{
|
||||
"id": "employer",
|
||||
"label": "We pay some people to do our work",
|
||||
"help": "Wages or salary, including worker-owners on payroll. Leave it unticked if everyone is a volunteer or a contractor"
|
||||
},
|
||||
{
|
||||
"id": "sponsor",
|
||||
"label": "Another organization holds our money or legal status",
|
||||
"help": "Such as a fiscal sponsor, a parent nonprofit or a church that takes donations for us"
|
||||
},
|
||||
{
|
||||
"id": "records",
|
||||
"label": "We keep information about the people we serve",
|
||||
"help": "Such as names, addresses, needs, health details or intake notes. A list of our own members does not count"
|
||||
},
|
||||
{
|
||||
"id": "risky",
|
||||
"label": "Our work carries physical risk, or needs a license, permit or inspection",
|
||||
"help": "Such as medical care, building, driving, cooking for the public or caring for children"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"next": "Next",
|
||||
"continue": "Continue",
|
||||
"skip": "Skip",
|
||||
"skipForNow": "Skip for now",
|
||||
"saveLater": "Skip for now",
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"_comment": "Create flow, the setup questions screen after the four setup questions (Figma CR-291, frame 24715:27505) and the setup copy the real-group review changed. Copy agreed 24 Oct. The question ids are the trait keys in _jobs.json.",
|
||||
"title": "A few more things about your group",
|
||||
"description": "Tick any that are true. Each one adds or changes a suggested card. We keep your answers with your draft and never publish them.",
|
||||
"continueButton": "Continue",
|
||||
"skipButton": "Skip",
|
||||
"questions": [
|
||||
{
|
||||
"id": "network",
|
||||
"label": "Some of our members or partners are groups",
|
||||
"help": "Such as crews in a network, organizations in a coalition or businesses in a co-op"
|
||||
},
|
||||
{
|
||||
"id": "nonvoters",
|
||||
"label": "Some people who do our work do not vote",
|
||||
"help": "Such as volunteers who are not members, staff who are not owners or contractors"
|
||||
},
|
||||
{
|
||||
"id": "employer",
|
||||
"label": "We pay some people to do our work",
|
||||
"help": "Wages or salary, including worker-owners on payroll. Leave it unticked if everyone is a volunteer or a contractor"
|
||||
},
|
||||
{
|
||||
"id": "sponsor",
|
||||
"label": "Another organization holds our money or legal status",
|
||||
"help": "Such as a fiscal sponsor, a parent nonprofit or a church that takes donations for us"
|
||||
},
|
||||
{
|
||||
"id": "records",
|
||||
"label": "We keep information about the people we serve",
|
||||
"help": "Such as names, addresses, needs, health details or intake notes. A list of our own members does not count"
|
||||
},
|
||||
{
|
||||
"id": "risky",
|
||||
"label": "Our work carries physical risk, or needs a license, permit or inspection",
|
||||
"help": "Such as medical care, building, driving, cooking for the public or caring for children"
|
||||
}
|
||||
],
|
||||
"sizeHelp": "Count the people who have a say in your big decisions. If others take part without a vote, you can say so later in setup.",
|
||||
"kind": {
|
||||
"title": "What kind of group are you, mainly?",
|
||||
"hint": "Pick one.",
|
||||
"othersTitle": "Anything else that fits?",
|
||||
"othersHelp": "These help us choose between cards and never add cards on their own."
|
||||
},
|
||||
"supermajorityShare": {
|
||||
"_comment": "Draft copy, waits on Dani’s sign-off. The question itself was agreed 27 Oct, calls 3 and 14. Nothing is preselected. A group that starts from a template answers it at final review.",
|
||||
"title": "What share of your members must agree to your biggest decisions?",
|
||||
"help": "This is your supermajority share. Your Rule uses it for votes such as changing the Rule, ending a membership and emergencies. Cards that set their own share keep it.",
|
||||
"options": {
|
||||
"twoThirds": "Two thirds",
|
||||
"threeQuarters": "Three quarters",
|
||||
"everyone": "Everyone",
|
||||
"custom": "Another share"
|
||||
},
|
||||
"customLabel": "Share of active members who vote, as a percent",
|
||||
"warnEveryone": "With everyone, one member can block a change to your Rule or a way out of a deadlock.",
|
||||
"warnHalfOrBelow": "At half or below, a change to your Rule could pass without most of the members who vote."
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import createCommunityName from "./create/community/communityName.json";
|
||||
import createCommunityStructure from "./create/community/communityStructure.json";
|
||||
import createCommunityContext from "./create/community/communityContext.json";
|
||||
import createCommunitySize from "./create/community/communitySize.json";
|
||||
import createSetupQuestions from "./create/community/setupQuestions.json";
|
||||
import createCommunityUpload from "./create/community/communityUpload.json";
|
||||
import createCommunitySave from "./create/community/communitySave.json";
|
||||
import createReview from "./create/community/review.json";
|
||||
@@ -119,6 +120,7 @@ export default {
|
||||
communityStructure: createCommunityStructure,
|
||||
communityContext: createCommunityContext,
|
||||
communitySize: createCommunitySize,
|
||||
setupQuestions: createSetupQuestions,
|
||||
communityUpload: createCommunityUpload,
|
||||
communitySave: createCommunitySave,
|
||||
review: createReview,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { useLayoutEffect } from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { SetupQuestionsScreen } from "../../app/(app)/create/screens/select/SetupQuestionsScreen";
|
||||
import { useCreateFlow } from "../../app/(app)/create/context/CreateFlowContext";
|
||||
import type { CreateFlowState } from "../../app/(app)/create/types";
|
||||
import { TRAIT_KEYS } from "../../lib/create/deckTypes";
|
||||
import messages from "../../messages/en/index";
|
||||
import {
|
||||
cleanup,
|
||||
renderWithProviders as render,
|
||||
screen,
|
||||
} from "../utils/test-utils";
|
||||
|
||||
const copy = messages.create.community.setupQuestions;
|
||||
|
||||
function ScreenWithState({ initial }: { initial: CreateFlowState }) {
|
||||
const { replaceState } = useCreateFlow();
|
||||
useLayoutEffect(() => {
|
||||
replaceState(initial);
|
||||
}, [replaceState, initial]);
|
||||
return <SetupQuestionsScreen />;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("Create flow setup-questions page", () => {
|
||||
it("renders the six questions off by default with a help line under each", () => {
|
||||
render(<SetupQuestionsScreen />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", { name: copy.title }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(copy.description)).toBeInTheDocument();
|
||||
|
||||
for (const question of copy.questions) {
|
||||
const checkbox = screen.getByRole("checkbox", { name: question.label });
|
||||
expect(checkbox).toHaveAttribute("aria-checked", "false");
|
||||
expect(screen.getByText(question.help)).toBeInTheDocument();
|
||||
}
|
||||
expect(copy.questions).toHaveLength(6);
|
||||
expect(copy.questions.map((question) => question.id)).toEqual([
|
||||
...TRAIT_KEYS,
|
||||
]);
|
||||
});
|
||||
|
||||
it("ticks a question from the box or the help line", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SetupQuestionsScreen />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("checkbox", { name: copy.questions[2].label }),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: copy.questions[2].label }),
|
||||
).toHaveAttribute("aria-checked", "true");
|
||||
|
||||
await user.click(screen.getByText(copy.questions[0].help));
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: copy.questions[0].label }),
|
||||
).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
it("restores ticked answers from the draft", () => {
|
||||
render(
|
||||
<ScreenWithState
|
||||
initial={{ setupTraits: { employer: true, sponsor: false } }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: copy.questions[2].label }),
|
||||
).toHaveAttribute("aria-checked", "true");
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: copy.questions[3].label }),
|
||||
).toHaveAttribute("aria-checked", "false");
|
||||
});
|
||||
|
||||
it("does not ask about police, immigration enforcement, or protest", () => {
|
||||
const blob = `${copy.title} ${copy.description} ${copy.questions
|
||||
.map((question) => `${question.label} ${question.help}`)
|
||||
.join(" ")}`.toLowerCase();
|
||||
expect(blob).not.toMatch(/police/);
|
||||
expect(blob).not.toMatch(/immigration/);
|
||||
expect(blob).not.toMatch(/protest/);
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,17 @@ describe("buildPublishPayload", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("omits setup answers from the published Rule", () => {
|
||||
const r = buildPublishPayload({
|
||||
title: "Oak Park Commons",
|
||||
setupTraits: { network: true, employer: true, risky: false },
|
||||
});
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
expect(r.document).not.toHaveProperty("setupTraits");
|
||||
expect(JSON.stringify(r)).not.toContain("setupTraits");
|
||||
});
|
||||
|
||||
it("returns title and fallback Overview section when no sections", () => {
|
||||
const r = buildPublishPayload({ title: "Oak Park Commons" });
|
||||
expect(r.ok).toBe(true);
|
||||
|
||||
@@ -58,6 +58,9 @@ describe("createFlowLayoutTokens", () => {
|
||||
expect(getCreateFlowContentMaxClass({ step: "community-structure" })).toBe(
|
||||
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||
);
|
||||
expect(getCreateFlowContentMaxClass({ step: "setup-questions" })).toBe(
|
||||
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
|
||||
);
|
||||
expect(
|
||||
getCreateFlowContentMaxClass({ step: "communication-methods" }),
|
||||
).toBe(CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS);
|
||||
|
||||
@@ -17,7 +17,10 @@ describe("getProportionBarProgressForCreateFlowStep", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps the rest of Create Community onto part 1, skipping setup at 1-5", () => {
|
||||
it("maps the rest of Create Community onto part 1, with setup questions at 1-5", () => {
|
||||
expect(getProportionBarProgressForCreateFlowStep("setup-questions")).toBe(
|
||||
"1-5",
|
||||
);
|
||||
expect(getProportionBarProgressForCreateFlowStep("community-upload")).toBe(
|
||||
"1-6",
|
||||
);
|
||||
|
||||
@@ -69,6 +69,28 @@ describe("createFlowStateSchema", () => {
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts setup-questions as currentStep and true or false setupTraits", () => {
|
||||
const r = createFlowStateSchema.safeParse({
|
||||
currentStep: "setup-questions",
|
||||
setupTraits: {
|
||||
network: true,
|
||||
nonvoters: false,
|
||||
employer: true,
|
||||
sponsor: false,
|
||||
records: false,
|
||||
risky: true,
|
||||
},
|
||||
});
|
||||
expect(r.success).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects non-boolean setupTraits values", () => {
|
||||
const r = createFlowStateSchema.safeParse({
|
||||
setupTraits: { network: "yes" },
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects title that is too long", () => {
|
||||
const r = createFlowStateSchema.safeParse({ title: "x".repeat(600) });
|
||||
expect(r.success).toBe(false);
|
||||
|
||||
@@ -26,6 +26,11 @@ describe("create footer messages", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes Continue and Skip for the setup questions step", () => {
|
||||
expect(messages.create.footer.continue).toBe("Continue");
|
||||
expect(messages.create.footer.skip).toBe("Skip");
|
||||
});
|
||||
|
||||
it("exposes Skip and Skip for now for optional community steps", () => {
|
||||
expect(messages.create.footer.skip).toBe("Skip");
|
||||
expect(messages.create.footer.skipForNow).toBe("Skip for now");
|
||||
|
||||
@@ -69,6 +69,14 @@ describe("flowSteps", () => {
|
||||
expect(getNextStep("community-context")).toBe("community-size");
|
||||
});
|
||||
|
||||
it("places setup-questions after community-size and before the logo upload", () => {
|
||||
expect(getStepIndex("setup-questions")).toBe(5);
|
||||
expect(getNextStep("community-size")).toBe("setup-questions");
|
||||
expect(getNextStep("setup-questions")).toBe("community-upload");
|
||||
expect(getPreviousStep("community-upload")).toBe("setup-questions");
|
||||
expect(getPreviousStep("setup-questions")).toBe("community-size");
|
||||
});
|
||||
|
||||
it("skipCommunitySave bridges upload → review and review → upload", () => {
|
||||
const opts = { skipCommunitySave: true } as const;
|
||||
expect(getNextStep("community-upload", opts)).toBe("review");
|
||||
@@ -77,7 +85,8 @@ describe("flowSteps", () => {
|
||||
|
||||
it("skipCommunitySave does not change steps outside the save segment", () => {
|
||||
const opts = { skipCommunitySave: true } as const;
|
||||
expect(getNextStep("community-size", opts)).toBe("community-upload");
|
||||
expect(getNextStep("community-size", opts)).toBe("setup-questions");
|
||||
expect(getNextStep("setup-questions", opts)).toBe("community-upload");
|
||||
expect(getNextStep("review", opts)).toBe("core-values");
|
||||
expect(getPreviousStep("communication-methods", opts)).toBe("core-values");
|
||||
});
|
||||
@@ -86,6 +95,8 @@ describe("flowSteps", () => {
|
||||
const opts = { useWithoutChangesIdentityOnly: true } as const;
|
||||
expect(getNextStep("community-name", opts)).toBe("community-context");
|
||||
expect(getNextStep("community-context", opts)).toBe("community-upload");
|
||||
expect(getNextStep("community-size", opts)).toBe("community-upload");
|
||||
expect(getNextStep("setup-questions", opts)).toBe("community-upload");
|
||||
expect(getNextStep("community-upload", opts)).toBe("confirm-stakeholders");
|
||||
expect(getNextStep("confirm-stakeholders", opts)).toBe("final-review");
|
||||
expect(getPreviousStep("community-context", opts)).toBe("community-name");
|
||||
@@ -199,6 +210,7 @@ describe("flowSteps", () => {
|
||||
expect(shouldOfferCreateFlowSaveAndExit("informational")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-name")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-structure")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("setup-questions")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-upload")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-save")).toBe(true);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("review")).toBe(true);
|
||||
@@ -244,6 +256,7 @@ describe("flowSteps", () => {
|
||||
expect(createFlowStepUsesSelectSplitScroll("community-structure")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(createFlowStepUsesSelectSplitScroll("setup-questions")).toBe(true);
|
||||
expect(createFlowStepUsesSelectSplitScroll("communication-methods")).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -19,6 +19,13 @@ describe("maturity and chip aliases", () => {
|
||||
expect(answers.mainKind).toBe("workersCoop");
|
||||
});
|
||||
|
||||
it("passes ticked setup answers through as traits", () => {
|
||||
const answers = groupAnswersFromCreateFlowState({
|
||||
setupTraits: { employer: true, records: false },
|
||||
});
|
||||
expect(answers.traits).toEqual({ employer: true, records: false });
|
||||
});
|
||||
|
||||
it("loads Enterprise and Larger organization as Established", () => {
|
||||
const fromId = migrateLegacyCreateFlowState({
|
||||
selectedMaturityIds: ["4"],
|
||||
|
||||
@@ -91,6 +91,7 @@ describe("useTemplateReviewActions", () => {
|
||||
title: "Neighborhood",
|
||||
communityContext: "Stale description",
|
||||
currentStep: "confirm-stakeholders",
|
||||
setupTraits: { employer: true, network: false },
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -104,6 +105,7 @@ describe("useTemplateReviewActions", () => {
|
||||
expect(applied?.title).toBeUndefined();
|
||||
expect(applied?.communityContext).toBeUndefined();
|
||||
expect(applied?.currentStep).toBeUndefined();
|
||||
expect(applied?.setupTraits).toBeUndefined();
|
||||
expect(applied?.pendingTemplateAction?.mode).toBe("useWithoutChanges");
|
||||
});
|
||||
|
||||
@@ -114,6 +116,7 @@ describe("useTemplateReviewActions", () => {
|
||||
title: "Neighborhood",
|
||||
communityContext: "We meet weekly",
|
||||
templateReviewEntryFromCreateFlow: true,
|
||||
setupTraits: { employer: true },
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -129,7 +132,38 @@ describe("useTemplateReviewActions", () => {
|
||||
expect(router.push).toHaveBeenCalledWith("/create/confirm-stakeholders");
|
||||
expect(applied?.title).toBe("Neighborhood");
|
||||
expect(applied?.communityContext).toBe("We meet weekly");
|
||||
expect(applied?.setupTraits).toEqual({ employer: true });
|
||||
expect(applied?.pendingTemplateAction).toBeUndefined();
|
||||
expect(applied?.templateReviewBackSlug).toBe("mutual-aid-mondays");
|
||||
});
|
||||
|
||||
it("customize with a community name drops leftover setup answers", async () => {
|
||||
const { result } = renderActions({
|
||||
title: "Neighborhood",
|
||||
setupTraits: { employer: true },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleCustomize();
|
||||
});
|
||||
|
||||
expect(router.push).toHaveBeenCalledWith("/create/core-values");
|
||||
expect(updateState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ setupTraits: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it("customize without a community name leaves setup answers for the questionnaire", async () => {
|
||||
const { result } = renderActions({
|
||||
setupTraits: { employer: true },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleCustomize();
|
||||
});
|
||||
|
||||
expect(router.push).toHaveBeenCalledWith("/create/informational");
|
||||
const patch = updateState.mock.calls[0]?.[0] as CreateFlowState;
|
||||
expect(patch).not.toHaveProperty("setupTraits");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TRAIT_KEYS } from "../../lib/create/deckTypes";
|
||||
import {
|
||||
EMPTY_SETUP_TRAITS,
|
||||
hasTickedSetupTrait,
|
||||
setupTraitsFromTickedIds,
|
||||
tickedSetupTraitIds,
|
||||
} from "../../lib/create/setupTraits";
|
||||
|
||||
describe("setupTraits", () => {
|
||||
it("starts with every question off", () => {
|
||||
expect(TRAIT_KEYS.every((key) => EMPTY_SETUP_TRAITS[key] === false)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(hasTickedSetupTrait(undefined)).toBe(false);
|
||||
expect(hasTickedSetupTrait(EMPTY_SETUP_TRAITS)).toBe(false);
|
||||
expect(tickedSetupTraitIds(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it("round-trips ticked ids as true or false on every trait", () => {
|
||||
const traits = setupTraitsFromTickedIds(["employer", "records"]);
|
||||
expect(traits).toEqual({
|
||||
network: false,
|
||||
nonvoters: false,
|
||||
employer: true,
|
||||
sponsor: false,
|
||||
records: true,
|
||||
risky: false,
|
||||
});
|
||||
expect(tickedSetupTraitIds(traits)).toEqual(["employer", "records"]);
|
||||
expect(hasTickedSetupTrait(traits)).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores ids that are not setup questions", () => {
|
||||
expect(setupTraitsFromTickedIds(["employer", "police"])).toEqual({
|
||||
...EMPTY_SETUP_TRAITS,
|
||||
employer: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user