Fix magic-link verify URLs in email and related create-flow QA #70
@@ -15,7 +15,7 @@ import type { CoreValueDetailEntry } from "../../types";
|
||||
export interface CoreValueEditFieldsProps {
|
||||
value: CoreValueDetailEntry;
|
||||
onChange: (_next: CoreValueDetailEntry) => void;
|
||||
/** Disable meaning/signals. Create-flow core-values omits this; final-review locks until Customize. */
|
||||
/** Disable meaning/signals. Facet and final-review callers omit this so fields stay editable on open. */
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
*
|
||||
* Card click opens the Figma create modal (node `20246-15829`) with three
|
||||
* editable sections rendered by {@link CommunicationMethodEditFields}. The primary
|
||||
* action is **Add Platform** for an unselected card; a selected card in view mode has
|
||||
* no footer primary — **Remove** is available from the kebab (same behavior as legacy
|
||||
* action is **Add Platform** for an unselected card and **Save** for a selected
|
||||
* card. **Remove** is available from the kebab (same behavior as legacy
|
||||
* footer remove via {@link removeMethodCardFromFacetSelection}).
|
||||
*/
|
||||
|
||||
@@ -216,7 +216,7 @@ export function CommunicationMethodsScreen() {
|
||||
methodId: pendingCardId,
|
||||
meta: state.customMethodCardMetaById,
|
||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||
modalEditUnlocked: false,
|
||||
modalEditUnlocked: true,
|
||||
draftFieldBlocks,
|
||||
customFacetDetailsMatchPreset,
|
||||
}),
|
||||
|
||||
@@ -217,7 +217,7 @@ export function ConflictManagementScreen() {
|
||||
methodId: pendingCardId,
|
||||
meta: state.customMethodCardMetaById,
|
||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||
modalEditUnlocked: false,
|
||||
modalEditUnlocked: true,
|
||||
draftFieldBlocks,
|
||||
customFacetDetailsMatchPreset,
|
||||
}),
|
||||
|
||||
@@ -214,7 +214,7 @@ export function MembershipMethodsScreen() {
|
||||
methodId: pendingCardId,
|
||||
meta: state.customMethodCardMetaById,
|
||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||
modalEditUnlocked: false,
|
||||
modalEditUnlocked: true,
|
||||
draftFieldBlocks,
|
||||
customFacetDetailsMatchPreset,
|
||||
}),
|
||||
|
||||
@@ -250,7 +250,7 @@ export function DecisionApproachesScreen() {
|
||||
methodId: pendingCardId,
|
||||
meta: state.customMethodCardMetaById,
|
||||
fieldBlocksById: state.customMethodCardFieldBlocksById,
|
||||
modalEditUnlocked: false,
|
||||
modalEditUnlocked: true,
|
||||
draftFieldBlocks,
|
||||
customFacetDetailsMatchPreset,
|
||||
}),
|
||||
|
||||
@@ -80,7 +80,8 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
},
|
||||
};
|
||||
|
||||
// State styles (embedded: Figma 20736-12668 – borderless, darker grey block, white text)
|
||||
// Embedded (Figma 20736-12668): borderless grey block; default copy is
|
||||
// tertiary, primary on focus so seeded modal fields stay muted until edit.
|
||||
const getStateStyles = (): {
|
||||
textarea: string;
|
||||
label: string;
|
||||
@@ -89,13 +90,13 @@ const TextAreaContainer = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
||||
if (disabled) {
|
||||
return {
|
||||
textarea:
|
||||
"border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] cursor-not-allowed opacity-60",
|
||||
"border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-tertiary,#b4b4b4)] cursor-not-allowed opacity-60",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
}
|
||||
return {
|
||||
textarea:
|
||||
"border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-default-tertiary)] focus:ring-inset",
|
||||
"border-0 bg-[var(--color-surface-default-secondary)] text-[var(--color-content-default-tertiary,#b4b4b4)] placeholder:text-[var(--color-content-default-tertiary,#b4b4b4)] focus:text-[var(--color-content-default-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--color-border-default-tertiary)] focus:ring-inset",
|
||||
label: "text-[var(--color-content-default-secondary)]",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ export interface TextAreaProps extends Omit<
|
||||
showHelpIcon?: boolean;
|
||||
/**
|
||||
* Visual appearance. "embedded" matches Create modal sections (Figma 20736-12668):
|
||||
* borderless, darker grey background, white text. "default" is standard bordered input.
|
||||
* borderless, darker grey background, tertiary text in default (primary on
|
||||
* focus). "default" is the standard bordered input.
|
||||
* @default "default"
|
||||
*/
|
||||
appearance?: TextAreaAppearanceValue;
|
||||
|
||||
@@ -29,6 +29,9 @@ import { isCustomMethodCardId } from "./isCustomMethodCardId";
|
||||
* so meta-only wizard cards show policy copy instead of empty section editors.
|
||||
* Pass `customFacetDetailsMatchPreset: false` when the caller knows facet details
|
||||
* were edited or cloned from a filled preset.
|
||||
*
|
||||
* Create-flow facet screens and final-review pass `modalEditUnlocked: true` so
|
||||
* section fields stay editable on open (Customize is the kebab wizard).
|
||||
*/
|
||||
export function usesWizardFieldBlocksModalBody(args: {
|
||||
methodId: string;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import {
|
||||
renderWithProviders as render,
|
||||
screen,
|
||||
cleanup,
|
||||
within,
|
||||
} from "../utils/test-utils";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { ConflictManagementScreen } from "../../app/(app)/create/screens/card/ConflictManagementScreen";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("ConflictManagementScreen", () => {
|
||||
it("opens section fields editable without Customize", async () => {
|
||||
render(<ConflictManagementScreen />);
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", {
|
||||
name: /Peer Mediation: Trained members/,
|
||||
})[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const fields = within(dialog).getAllByRole("textbox");
|
||||
expect(fields.length).toBeGreaterThan(0);
|
||||
for (const field of fields) {
|
||||
expect(field).toBeEnabled();
|
||||
}
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole("button", { name: "More options" }),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("menuitem", { name: "Customize" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import {
|
||||
renderWithProviders as render,
|
||||
screen,
|
||||
cleanup,
|
||||
within,
|
||||
} from "../utils/test-utils";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { MembershipMethodsScreen } from "../../app/(app)/create/screens/card/MembershipMethodsScreen";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("MembershipMethodsScreen", () => {
|
||||
it("opens section fields editable without Customize", async () => {
|
||||
render(<MembershipMethodsScreen />);
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", {
|
||||
name: /Open Access: Maximum inclusion/,
|
||||
})[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const fields = within(dialog).getAllByRole("textbox");
|
||||
expect(fields.length).toBeGreaterThan(0);
|
||||
for (const field of fields) {
|
||||
expect(field).toBeEnabled();
|
||||
}
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole("button", { name: "More options" }),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("menuitem", { name: "Customize" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -42,4 +42,17 @@ describe("TextArea appearance", () => {
|
||||
expect(textarea).toBeInTheDocument();
|
||||
expect(textarea).toHaveClass("border-0");
|
||||
});
|
||||
|
||||
it("uses tertiary text in the embedded default state and primary on focus", () => {
|
||||
renderWithProviders(
|
||||
<TextArea label="Notes" value="Some text" appearance="embedded" />,
|
||||
);
|
||||
const textarea = screen.getByRole("textbox", { name: /notes/i });
|
||||
expect(textarea).toHaveClass(
|
||||
"text-[var(--color-content-default-tertiary,#b4b4b4)]",
|
||||
);
|
||||
expect(textarea).toHaveClass(
|
||||
"focus:text-[var(--color-content-default-primary)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -126,6 +126,33 @@ describe("Create flow communication-methods page", () => {
|
||||
expect(screen.getByRole("menuitem", { name: "Customize" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("meta-only custom policy opens with editable section fields", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CommunicationMethodsScreenWithState
|
||||
initial={{
|
||||
selectedCommunicationMethodIds: [CUSTOM_POLICY_ID],
|
||||
customMethodCardMetaById: {
|
||||
[CUSTOM_POLICY_ID]: { label: "My policy", supportText: "Desc" },
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getAllByRole("button", { name: /My policy: Desc/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const textboxes = within(dialog).getAllByRole("textbox");
|
||||
expect(textboxes.length).toBeGreaterThan(0);
|
||||
for (const field of textboxes) {
|
||||
expect(field).not.toBeDisabled();
|
||||
}
|
||||
expect(
|
||||
within(dialog).queryByText("No custom fields yet."),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders without error", () => {
|
||||
render(<CommunicationMethodsScreen />);
|
||||
|
||||
|
||||
@@ -201,6 +201,29 @@ describe("Create flow decision-approaches page", () => {
|
||||
).toHaveTextContent("SELECTED");
|
||||
});
|
||||
|
||||
test("opens approach fields editable without Customize", 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");
|
||||
const textboxes = within(dialog).getAllByRole("textbox");
|
||||
expect(textboxes.length).toBeGreaterThan(0);
|
||||
for (const field of textboxes) {
|
||||
expect(field).not.toBeDisabled();
|
||||
}
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", { name: "More options" }),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("menuitem", { name: "Customize" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("clicking a card opens the create modal and confirming selects it", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
Reference in New Issue
Block a user