Call the last create-flow action Publish so it reads as putting a rule on the web, not locking it forever.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-02 16:27:57 -06:00
co-authored by Cursor
parent db34a1f0df
commit f91ac7a893
16 changed files with 44 additions and 26 deletions
+3 -3
View File
@@ -593,7 +593,7 @@ function CreateFlowLayoutContent({
key: "publish", key: "publish",
status: "danger" as const, status: "danger" as const,
title: title:
messages.create.reviewAndComplete.publish.finalizeBannerTitle, messages.create.reviewAndComplete.publish.publishBannerTitle,
description: publishBannerMessage, description: publishBannerMessage,
onClose: () => setPublishBannerMessage(null), onClose: () => setPublishBannerMessage(null),
} }
@@ -956,8 +956,8 @@ function CreateFlowLayoutContent({
{isFinalReviewLike {isFinalReviewLike
? isPublishing ? isPublishing
? messages.create.reviewAndComplete.publish ? messages.create.reviewAndComplete.publish
.finalizeButtonPublishing .publishButtonPublishing
: footer.finalizeCommunityRule : footer.publishCommunityRule
: getDefaultFooterLabel(currentStep, footer)} : getDefaultFooterLabel(currentStep, footer)}
</Button> </Button>
) : null ) : null
@@ -47,7 +47,7 @@ export function useCreateFlowFinalize({
/** Session gate return path (`?syncDraft=1`) — differs for `/create/edit-rule` vs `/create/final-review`. */ /** Session gate return path (`?syncDraft=1`) — differs for `/create/edit-rule` vs `/create/final-review`. */
loginReturnPath: string; loginReturnPath: string;
/** /**
* `undefined` while `/api/auth/session` is in flight — finalize is a no-op * `undefined` while `/api/auth/session` is in flight — publish is a no-op
* until it resolves. `null` is a guest (anonymous publish). * until it resolves. `null` is a guest (anonymous publish).
*/ */
sessionUser: SessionUser | null | undefined; sessionUser: SessionUser | null | undefined;
+1 -1
View File
@@ -222,7 +222,7 @@ export interface CreateFlowState {
*/ */
templateReviewEntryFromCreateFlow?: boolean; templateReviewEntryFromCreateFlow?: boolean;
/** /**
* When set, **Finalize** and signed-in **Save & Exit** update this published * When set, **Publish** and signed-in **Save & Exit** update this published
* rule (PATCH) instead of POSTing a new rule or only saving a draft. * rule (PATCH) instead of POSTing a new rule or only saving a draft.
*/ */
editingPublishedRuleId?: string; editingPublishedRuleId?: string;
@@ -10,8 +10,8 @@ type FooterMessages = typeof footerMessages;
* from this map fall back to `footer.next`. * from this map fall back to `footer.next`.
* *
* `final-review` is handled separately by the caller because its label * `final-review` is handled separately by the caller because its label
* also depends on the in-flight publish flag (`finalizeButtonPublishing` * also depends on the in-flight publish flag (`publishButtonPublishing`
* vs `finalizeCommunityRule`). * vs `publishCommunityRule`).
*/ */
const DEFAULT_FOOTER_LABEL_BY_STEP: ReadonlyMap< const DEFAULT_FOOTER_LABEL_BY_STEP: ReadonlyMap<
CreateFlowStep, CreateFlowStep,
+1 -1
View File
@@ -202,7 +202,7 @@ export const TEMPLATE_REVIEW_FROM_CREATE_FLOW_VALUE = "1" as const;
export const TEMPLATES_FACET_RECOMMEND_QUERY = "recommendTemplates" as const; export const TEMPLATES_FACET_RECOMMEND_QUERY = "recommendTemplates" as const;
export const TEMPLATES_FACET_RECOMMEND_VALUE = "1" as const; export const TEMPLATES_FACET_RECOMMEND_VALUE = "1" as const;
/** `/create/completed?celebrate=1` — post-finalize toast; set only after **initial** POST publish, not PATCH updates. */ /** `/create/completed?celebrate=1` — post-publish toast; set only after **initial** POST publish, not PATCH updates. */
export const CREATE_FLOW_COMPLETED_CELEBRATE_QUERY = "celebrate" as const; export const CREATE_FLOW_COMPLETED_CELEBRATE_QUERY = "celebrate" as const;
export const CREATE_FLOW_COMPLETED_CELEBRATE_VALUE = "1" as const; export const CREATE_FLOW_COMPLETED_CELEBRATE_VALUE = "1" as const;
+2 -2
View File
@@ -105,8 +105,8 @@ Only one `?fromFlow=1` marker exists, on one hop (`/create/review` → `/templat
| Mode | Where progress lives | Save & Exit / publish | | Mode | Where progress lives | Save & Exit / publish |
| --- | --- | --- | | --- | --- | --- |
| **Anonymous** | `localStorage` key **`create-flow-anonymous`** | **Save & Exit** (from `community-structure` onward, plus `edit-rule`) opens the save-progress magic-link modal; after verify, optional **PUT** `/api/drafts/me` when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` (see Tickets 45 in [guides/backend-linear-tickets.md](guides/backend-linear-tickets.md)). **Finalize** `POST`s `/api/rules` without a session (`userId` null) and sets httpOnly **`cr_rule_claim`**. Landing on **`/create/completed`** opens a **keep-this-rule** magic-link modal (no draft transfer); the top nav stays **Save & Exit** so they can reopen it (**Continue without saving** from that control leaves the flow). After Finalize, skip on the auto-opened modal stays on completed. **Edit** shows when a last-published rule is in session storage. The public URL works; the row is omitted from `GET /api/rules` until claimed. Signing in on the same browser attaches `userId` (profile, edit, invites). | | **Anonymous** | `localStorage` key **`create-flow-anonymous`** | **Save & Exit** (from `community-structure` onward, plus `edit-rule`) opens the save-progress magic-link modal; after verify, optional **PUT** `/api/drafts/me` when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` (see Tickets 45 in [guides/backend-linear-tickets.md](guides/backend-linear-tickets.md)). **Publish** `POST`s `/api/rules` without a session (`userId` null) and sets httpOnly **`cr_rule_claim`**. Landing on **`/create/completed`** opens a **keep-this-rule** magic-link modal (no draft transfer); the top nav stays **Save & Exit** so they can reopen it (**Continue without saving** from that control leaves the flow). After Publish, skip on the auto-opened modal stays on completed. **Edit** shows when a last-published rule is in session storage. The public URL works; the row is omitted from `GET /api/rules` until claimed. Signing in on the same browser attaches `userId` (profile, edit, invites). |
| **Signed-in** | In-memory React state in **`CreateFlowContext`** | **Save & Exit** from the **`community-structure`** step onward (and `edit-rule`) may **PUT** `/api/drafts/me` when sync is on. **Completed** is **Exit**. **Finalize** stores the rule with **`userId`**. **Sign out** is on profile, not in the create top nav. | | **Signed-in** | In-memory React state in **`CreateFlowContext`** | **Save & Exit** from the **`community-structure`** step onward (and `edit-rule`) may **PUT** `/api/drafts/me` when sync is on. **Completed** is **Exit**. **Publish** stores the rule with **`userId`**. **Sign out** is on profile, not in the create top nav. |
Details and edge cases (conflict confirm, banners, `?syncDraft=1`) match **Ticket 4**, **Ticket 5**, and [`docs/guides/backend-roadmap.md`](guides/backend-roadmap.md) §12. Details and edge cases (conflict confirm, banners, `?syncDraft=1`) match **Ticket 4**, **Ticket 5**, and [`docs/guides/backend-roadmap.md`](guides/backend-roadmap.md) §12.
+3 -3
View File
@@ -218,19 +218,19 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
**Goal:** Completing the flow persists a **PublishedRule** via existing [publishRule](lib/create/api.ts). **Goal:** Completing the flow persists a **PublishedRule** via existing [publishRule](lib/create/api.ts).
**Context:** [lib/create/api.ts](lib/create/api.ts) wraps `POST /api/rules` with Zod-validated body (Ticket 2). Finalize flows through [useCreateFlowFinalize](app/(app)/create/hooks/useCreateFlowFinalize.ts) from [CreateFlowLayoutClient](app/(app)/create/CreateFlowLayoutClient.tsx) (`final-review``publishRule``/create/completed`). **Context:** [lib/create/api.ts](lib/create/api.ts) wraps `POST /api/rules` with Zod-validated body (Ticket 2). Publish flows through [useCreateFlowFinalize](app/(app)/create/hooks/useCreateFlowFinalize.ts) from [CreateFlowLayoutClient](app/(app)/create/CreateFlowLayoutClient.tsx) (`final-review``publishRule``/create/completed`).
**Implementation (shipped):** **Implementation (shipped):**
1. Map `CreateFlowState``title` / `summary` / `document` via [buildPublishPayload](lib/create/buildPublishPayload.ts) (and related builders). 1. Map `CreateFlowState``title` / `summary` / `document` via [buildPublishPayload](lib/create/buildPublishPayload.ts) (and related builders).
2. Call `publishRule` on explicit **Finalize** from `final-review` ([useCreateFlowFinalize](app/(app)/create/hooks/useCreateFlowFinalize.ts)). 2. Call `publishRule` on explicit **Publish** from `final-review` ([useCreateFlowFinalize](app/(app)/create/hooks/useCreateFlowFinalize.ts)).
3. **401**`openLogin` with return path (Ticket 3 / `AuthModalProvider`). 3. **401**`openLogin` with return path (Ticket 3 / `AuthModalProvider`).
4. Success: navigate to `completed` with rule id in query string. 4. Success: navigate to `completed` with rule id in query string.
**Acceptance criteria:** **Acceptance criteria:**
- [x] Published row appears in Postgres (`PublishedRule`) and `GET /api/rules` lists it. - [x] Published row appears in Postgres (`PublishedRule`) and `GET /api/rules` lists it.
- [x] User sees clear success/failure (banner / flow state; see finalize hook). - [x] User sees clear success/failure (banner / flow state; see publish hook).
**Files:** [app/(app)/create/hooks/useCreateFlowFinalize.ts](app/(app)/create/hooks/useCreateFlowFinalize.ts), [CreateFlowLayoutClient.tsx](app/(app)/create/CreateFlowLayoutClient.tsx), [app/api/rules/route.ts](app/api/rules/route.ts), [lib/create/api.ts](lib/create/api.ts), [lib/create/buildPublishPayload.ts](lib/create/buildPublishPayload.ts). **Files:** [app/(app)/create/hooks/useCreateFlowFinalize.ts](app/(app)/create/hooks/useCreateFlowFinalize.ts), [CreateFlowLayoutClient.tsx](app/(app)/create/CreateFlowLayoutClient.tsx), [app/api/rules/route.ts](app/api/rules/route.ts), [lib/create/api.ts](lib/create/api.ts), [lib/create/buildPublishPayload.ts](lib/create/buildPublishPayload.ts).
+2 -2
View File
@@ -1,7 +1,7 @@
/** /**
* Guest finalize `/create/completed` keep-this-rule login. Stored in * Guest publish `/create/completed` keep-this-rule login. Stored in
* sessionStorage so the prompt survives create-layout remounts and is not * sessionStorage so the prompt survives create-layout remounts and is not
* tied to the Finalize click (that click would otherwise dismiss the overlay). * tied to the Publish click (that click would otherwise dismiss the overlay).
*/ */
export const CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY = export const CREATE_FLOW_PENDING_KEEP_RULE_LOGIN_KEY =
"createFlow.pendingKeepRuleLogin"; "createFlow.pendingKeepRuleLogin";
+1 -1
View File
@@ -9,7 +9,7 @@
"confirmDetails": "Confirm details", "confirmDetails": "Confirm details",
"confirmDescription": "Confirm description", "confirmDescription": "Confirm description",
"confirmMembers": "Confirm members", "confirmMembers": "Confirm members",
"finalizeCommunityRule": "Finalize CommunityRule", "publishCommunityRule": "Publish CommunityRule",
"confirmStakeholders": "Confirm Stakeholders", "confirmStakeholders": "Confirm Stakeholders",
"confirmCoreValues": "Confirm values", "confirmCoreValues": "Confirm values",
"confirmCommunication": "Confirm", "confirmCommunication": "Confirm",
@@ -1,8 +1,8 @@
{ {
"title": "Review your CommunityRule", "title": "Review your CommunityRule",
"description": "Here's what other people will see. Make sure everything looks good before you finalize everything. Once the rule is finalized, you must use one of your decision-making mechanisms to edit it again.", "description": "Here's what other people will see. Make sure everything looks good before you publish. Once the rule is published, you must use one of your decision-making mechanisms to edit it again.",
"editPublishedTitle": "Edit your CommunityRule", "editPublishedTitle": "Edit your CommunityRule",
"editPublishedDescription": "Update what others see on your public rule. Save & Exit or Finalize applies changes to your published CommunityRule.", "editPublishedDescription": "Update what others see on your public rule. Save & Exit or Publish applies changes to your published CommunityRule.",
"ruleCardTitleFallback": "Your community", "ruleCardTitleFallback": "Your community",
"chipEditModal": { "chipEditModal": {
"saveButton": "Save", "saveButton": "Save",
@@ -1,6 +1,6 @@
{ {
"finalizeBannerTitle": "Couldn't publish", "publishBannerTitle": "Couldn't publish",
"missingCommunityName": "Add a community name before finalizing.", "missingCommunityName": "Add a community name before publishing.",
"finalizeButtonPublishing": "Publishing…", "publishButtonPublishing": "Publishing…",
"genericPublishFailed": "Something went wrong. Try again." "genericPublishFailed": "Something went wrong. Try again."
} }
+1 -1
View File
@@ -8,7 +8,7 @@ export default {
docs: { docs: {
description: { description: {
component: component:
"Pre-finalize review: HeaderLockup + expanded Rule sections.", "Pre-publish review: HeaderLockup + expanded Rule sections.",
}, },
}, },
}, },
+2 -2
View File
@@ -84,7 +84,7 @@ describe("CompletedScreen", () => {
expect(screen.getByText("Fixture value title")).toBeInTheDocument(); expect(screen.getByText("Fixture value title")).toBeInTheDocument();
}); });
it("does not show post-finalize toast without celebrate query", () => { it("does not show post-publish toast without celebrate query", () => {
render(<CompletedScreen />); render(<CompletedScreen />);
expect( expect(
screen.queryByText( screen.queryByText(
@@ -98,7 +98,7 @@ describe("CompletedScreen", () => {
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}); });
it("shows post-finalize toast in status region when celebrate query is set", () => { it("shows post-publish toast in status region when celebrate query is set", () => {
mockSearchParams({ mockSearchParams({
[CREATE_FLOW_COMPLETED_CELEBRATE_QUERY]: [CREATE_FLOW_COMPLETED_CELEBRATE_QUERY]:
CREATE_FLOW_COMPLETED_CELEBRATE_VALUE, CREATE_FLOW_COMPLETED_CELEBRATE_VALUE,
+13 -1
View File
@@ -79,7 +79,7 @@ describe("FinalReviewScreen", () => {
render(<FinalReviewScreen />); render(<FinalReviewScreen />);
expect( expect(
screen.getByText( screen.getByText(
/Here's what other people will see. Make sure everything looks good before you finalize everything. Once the rule is finalized, you must use one of your decision-making mechanisms to edit it again./i, /Here's what other people will see. Make sure everything looks good before you publish. Once the rule is published, you must use one of your decision-making mechanisms to edit it again./i,
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -1004,6 +1004,18 @@ function FinalReviewEditPublishedWithStateProbe({
} }
describe("FinalReviewScreen — edit published title and description", () => { describe("FinalReviewScreen — edit published title and description", () => {
it("renders edit-published lockup copy", () => {
render(<FinalReviewScreen variant="editPublished" />);
expect(
screen.getByRole("heading", { name: "Edit your CommunityRule" }),
).toBeInTheDocument();
expect(
screen.getByText(
/Update what others see on your public rule. Save & Exit or Publish applies changes to your published CommunityRule./i,
),
).toBeInTheDocument();
});
it("does not expose click-to-edit title or description on default final review", () => { it("does not expose click-to-edit title or description on default final review", () => {
render( render(
<FinalReviewWithFlowState <FinalReviewWithFlowState
+6
View File
@@ -19,4 +19,10 @@ describe("create footer messages", () => {
it("exposes confirmMembers for the community-size footer CTA", () => { it("exposes confirmMembers for the community-size footer CTA", () => {
expect(messages.create.footer.confirmMembers).toBe("Confirm members"); expect(messages.create.footer.confirmMembers).toBe("Confirm members");
}); });
it("exposes publishCommunityRule for the final-review footer CTA", () => {
expect(messages.create.footer.publishCommunityRule).toBe(
"Publish CommunityRule",
);
});
}); });
@@ -258,7 +258,7 @@ describe("useCreateFlowFinalize", () => {
}); });
}); });
it("does not PATCH when a guest finalizes an already-published rule", async () => { it("does not PATCH when a guest publishes an already-published rule", async () => {
const { result } = renderHook(() => const { result } = renderHook(() =>
useCreateFlowFinalize({ useCreateFlowFinalize({
state: { state: {