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:
@@ -593,7 +593,7 @@ function CreateFlowLayoutContent({
|
||||
key: "publish",
|
||||
status: "danger" as const,
|
||||
title:
|
||||
messages.create.reviewAndComplete.publish.finalizeBannerTitle,
|
||||
messages.create.reviewAndComplete.publish.publishBannerTitle,
|
||||
description: publishBannerMessage,
|
||||
onClose: () => setPublishBannerMessage(null),
|
||||
}
|
||||
@@ -956,8 +956,8 @@ function CreateFlowLayoutContent({
|
||||
{isFinalReviewLike
|
||||
? isPublishing
|
||||
? messages.create.reviewAndComplete.publish
|
||||
.finalizeButtonPublishing
|
||||
: footer.finalizeCommunityRule
|
||||
.publishButtonPublishing
|
||||
: footer.publishCommunityRule
|
||||
: getDefaultFooterLabel(currentStep, footer)}
|
||||
</Button>
|
||||
) : null
|
||||
|
||||
@@ -47,7 +47,7 @@ export function useCreateFlowFinalize({
|
||||
/** Session gate return path (`?syncDraft=1`) — differs for `/create/edit-rule` vs `/create/final-review`. */
|
||||
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).
|
||||
*/
|
||||
sessionUser: SessionUser | null | undefined;
|
||||
|
||||
@@ -222,7 +222,7 @@ export interface CreateFlowState {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
editingPublishedRuleId?: string;
|
||||
|
||||
@@ -10,8 +10,8 @@ type FooterMessages = typeof footerMessages;
|
||||
* from this map fall back to `footer.next`.
|
||||
*
|
||||
* `final-review` is handled separately by the caller because its label
|
||||
* also depends on the in-flight publish flag (`finalizeButtonPublishing`
|
||||
* vs `finalizeCommunityRule`).
|
||||
* also depends on the in-flight publish flag (`publishButtonPublishing`
|
||||
* vs `publishCommunityRule`).
|
||||
*/
|
||||
const DEFAULT_FOOTER_LABEL_BY_STEP: ReadonlyMap<
|
||||
CreateFlowStep,
|
||||
|
||||
@@ -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_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_VALUE = "1" as const;
|
||||
|
||||
|
||||
+2
-2
@@ -105,8 +105,8 @@ Only one `?fromFlow=1` marker exists, on one hop (`/create/review` → `/templat
|
||||
|
||||
| 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 4–5 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). |
|
||||
| **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. |
|
||||
| **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 4–5 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**. **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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
**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):**
|
||||
|
||||
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`).
|
||||
4. Success: navigate to `completed` with rule id in query string.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- [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).
|
||||
|
||||
|
||||
@@ -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
|
||||
* 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 =
|
||||
"createFlow.pendingKeepRuleLogin";
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"confirmDetails": "Confirm details",
|
||||
"confirmDescription": "Confirm description",
|
||||
"confirmMembers": "Confirm members",
|
||||
"finalizeCommunityRule": "Finalize CommunityRule",
|
||||
"publishCommunityRule": "Publish CommunityRule",
|
||||
"confirmStakeholders": "Confirm Stakeholders",
|
||||
"confirmCoreValues": "Confirm values",
|
||||
"confirmCommunication": "Confirm",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"chipEditModal": {
|
||||
"saveButton": "Save",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"finalizeBannerTitle": "Couldn't publish",
|
||||
"missingCommunityName": "Add a community name before finalizing.",
|
||||
"finalizeButtonPublishing": "Publishing…",
|
||||
"publishBannerTitle": "Couldn't publish",
|
||||
"missingCommunityName": "Add a community name before publishing.",
|
||||
"publishButtonPublishing": "Publishing…",
|
||||
"genericPublishFailed": "Something went wrong. Try again."
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Pre-finalize review: HeaderLockup + expanded Rule sections.",
|
||||
"Pre-publish review: HeaderLockup + expanded Rule sections.",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -84,7 +84,7 @@ describe("CompletedScreen", () => {
|
||||
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 />);
|
||||
expect(
|
||||
screen.queryByText(
|
||||
@@ -98,7 +98,7 @@ describe("CompletedScreen", () => {
|
||||
).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({
|
||||
[CREATE_FLOW_COMPLETED_CELEBRATE_QUERY]:
|
||||
CREATE_FLOW_COMPLETED_CELEBRATE_VALUE,
|
||||
|
||||
@@ -79,7 +79,7 @@ describe("FinalReviewScreen", () => {
|
||||
render(<FinalReviewScreen />);
|
||||
expect(
|
||||
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();
|
||||
});
|
||||
@@ -1004,6 +1004,18 @@ function FinalReviewEditPublishedWithStateProbe({
|
||||
}
|
||||
|
||||
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", () => {
|
||||
render(
|
||||
<FinalReviewWithFlowState
|
||||
|
||||
@@ -19,4 +19,10 @@ describe("create footer messages", () => {
|
||||
it("exposes confirmMembers for the community-size footer CTA", () => {
|
||||
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(() =>
|
||||
useCreateFlowFinalize({
|
||||
state: {
|
||||
|
||||
Reference in New Issue
Block a user