create-ailk 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +11 -3
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +32 -0
- package/dist/lib/scaffold-render-sweep.js +76 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +128 -3
- package/dist/programmatic.js +11 -1
- package/dist/site-roster.js +21 -4
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.d.ts +14 -0
- package/dist/surfaces.js +170 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +7 -2
- package/dist/topology.js +88 -3
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/.env.example +17 -1
- package/templates/apps/api/CLAUDE.md +31 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +17 -0
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
- package/templates/apps/api/src/lib/consent-store.ts +335 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +176 -3
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +272 -39
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/middleware/auth.ts +214 -46
- package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
- package/templates/apps/api/src/routes/checkout/session.ts +3 -3
- package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
- package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
- package/templates/apps/api/src/routes/consent/index.ts +18 -0
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
- package/templates/apps/api/src/routes/leads/index.ts +19 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
- package/templates/apps/api/src/server.ts +218 -16
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +173 -4
- package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
- package/templates/apps/api/src/services/consent.ts +488 -3
- package/templates/apps/api/src/services/lead-routing.ts +135 -14
- package/templates/apps/api/src/services/person.ts +501 -1
- package/templates/apps/api/src/services/project-listings.ts +7 -2
- package/templates/apps/api/src/services/request-confirm.ts +52 -0
- package/templates/apps/api/src/services/revalidation.ts +11 -1
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +9 -4
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/boot.ts +5 -23
- package/templates/apps/mcp/src/http-transport.ts +93 -73
- package/templates/apps/mcp/src/index.ts +2 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +302 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +724 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +346 -41
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +232 -29
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +140 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +409 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +65 -380
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +188 -54
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +174 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +30 -4
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +25 -2
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/auth-email.ts +45 -10
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/topology.generated.ts +33 -0
- package/templates/apps/web/messages/en.json +194 -33
- package/templates/database/CHANGELOG.md +37 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
- package/templates/database/__tests__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -0
- package/templates/database/__tests__/person-contact.db.test.ts +563 -0
- package/templates/database/__tests__/person.db.test.ts +3 -1
- package/templates/database/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +166 -25
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
- package/templates/database/package.json +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use client"; // the shell owns collapse state; sign-out is a client-side Better Auth call + router navigation
|
|
1
|
+
"use client"; // the shell owns collapse state; sign-out is a client-side hub-session POST or Better Auth call + router navigation
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* AccountFrame — the account area's SignedInShell composition (#7430, spec
|
|
@@ -7,8 +7,28 @@
|
|
|
7
7
|
* what only the client has: the current path and the sign-out call.
|
|
8
8
|
*
|
|
9
9
|
* `createAuthClient()` at module scope is the `sign-in-gate.tsx` idiom. Log
|
|
10
|
-
* out ends the session, then navigates
|
|
11
|
-
*
|
|
10
|
+
* out ends the session, then navigates and refreshes so every RSC under the
|
|
11
|
+
* `(authed)` gate re-reads the (now absent) session.
|
|
12
|
+
*
|
|
13
|
+
* #8465 — a spoke that signs in through its hub (#7894) holds its session in
|
|
14
|
+
* a sealed cookie only `POST /api/hub-session/sign-out` clears (it also
|
|
15
|
+
* revokes the credential at the hub); Better Auth's `signOut()` hits a route
|
|
16
|
+
* that spoke does not serve. The server layout reads the generated seam's
|
|
17
|
+
* `SIGN_OUT_ENDPOINT` and hands it down as `signOutEndpoint` — the signed-in
|
|
18
|
+
* Nav's own pattern, because a client component cannot import the seam. With
|
|
19
|
+
* an endpoint, Log out POSTs it and navigates to the sign-in page only when
|
|
20
|
+
* the answer was 2xx; a refused or failed sign-out (a 403 on an Origin
|
|
21
|
+
* mismatch, a 5xx, a network error) leaves the cookie valid, so the frame
|
|
22
|
+
* stays on the page and says so, never routing away with a live session.
|
|
23
|
+
* Without an endpoint (a hub or a single site) nothing changes: Better Auth,
|
|
24
|
+
* then home.
|
|
25
|
+
*
|
|
26
|
+
* #8231 D1, as amended by #8334 — the rail reads Inbox, a Library group
|
|
27
|
+
* holding Artifacts, and the footer's ?. Settings is not a rail row: it lives
|
|
28
|
+
* in the avatar menu, as on the Admin (the operator's 2026-10-11 Phase 6
|
|
29
|
+
* read), so no row is selected on a Settings page. A row is selected on its
|
|
30
|
+
* own path and the paths under it; the Inbox also on an item's page
|
|
31
|
+
* (/account/inbox/…).
|
|
12
32
|
*
|
|
13
33
|
* #7469 (DESIGN.md §2b as amended 2026-10-01, matching the Admin's §5.15) —
|
|
14
34
|
* below `md` the bar reads avatar · Inbox · For you · Help, with no count
|
|
@@ -16,6 +36,27 @@
|
|
|
16
36
|
* frame reads the URL and hands the tab to the page (`PhoneTabProvider`),
|
|
17
37
|
* which shows one panel per tab. At `md`+ nothing changes — For you stays
|
|
18
38
|
* the aside.
|
|
39
|
+
*
|
|
40
|
+
* #8233 D4 — on the Inbox route itself the phone top bar's trailing cell is
|
|
41
|
+
* an empty node handed to the page (`NewMenuSlotProvider`), which portals
|
|
42
|
+
* its compact New into it; every other path has no trailing cell.
|
|
43
|
+
*
|
|
44
|
+
* #8337 D4 — the `data-ailk-shell="account"` marker on a `display: contents`
|
|
45
|
+
* wrapper around `SignedInShell`: the layout's `shell-vars` reset
|
|
46
|
+
* (`:root:has([data-ailk-shell])`) matches only while this frame is in the
|
|
47
|
+
* document, so the Admin's colour tokens reach the shell and everything it
|
|
48
|
+
* portals to `document.body` (the avatar/help menus, For-you, New, the phone
|
|
49
|
+
* sheets) without the wrapper adding a box — `contents` keeps the shell's own
|
|
50
|
+
* layout (`min-h-screen`) unchanged.
|
|
51
|
+
*
|
|
52
|
+
* #8430 — the User settings routes take the screen as the Admin's do: the
|
|
53
|
+
* frame renders the page alone, inside the marker only — no rail, no Inbox,
|
|
54
|
+
* no AppShell — and the page draws the shared `AccountSettingsFrame` in its
|
|
55
|
+
* standalone shape, its own column flush and full height with Back to the
|
|
56
|
+
* Inbox at its top. The marker stays, so the Admin's tokens still reach the
|
|
57
|
+
* settings in both modes. Which routes those are is `isSettingsPath` — an
|
|
58
|
+
* exact match on `SETTINGS_ROUTES`, the list the settings tabs read too
|
|
59
|
+
* (#8441), so a sub-route stays inside the shell.
|
|
19
60
|
*/
|
|
20
61
|
|
|
21
62
|
import {
|
|
@@ -31,13 +72,15 @@ import {
|
|
|
31
72
|
type SignedInShellProps,
|
|
32
73
|
} from "@working-theory/templates/shells";
|
|
33
74
|
import { localeHref } from "@working-theory/ui";
|
|
34
|
-
import { Link } from "@working-theory/ui/primitives";
|
|
75
|
+
import { InlineMessage, Link } from "@working-theory/ui/primitives";
|
|
35
76
|
import { createAuthClient } from "better-auth/react";
|
|
36
|
-
import { Inbox, Settings, Sparkles } from "lucide-react";
|
|
77
|
+
import { Inbox, Layers, Settings, Sparkles } from "lucide-react";
|
|
37
78
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
38
|
-
import type
|
|
79
|
+
import { useState, type ReactElement, type ReactNode } from "react";
|
|
39
80
|
|
|
40
81
|
import { ACCOUNT_PERSIST_KEY } from "./for-you-labels";
|
|
82
|
+
import { NewMenuSlotProvider } from "./new-menu";
|
|
83
|
+
import { isSettingsPath } from "./settings-frame";
|
|
41
84
|
|
|
42
85
|
const authClient = createAuthClient();
|
|
43
86
|
|
|
@@ -51,6 +94,11 @@ export interface AccountFrameLabels extends SignedInShellLabels {
|
|
|
51
94
|
/** The phone's For you tab label (#7469). */
|
|
52
95
|
forYou: string;
|
|
53
96
|
settings: string;
|
|
97
|
+
/** The rail's Library group heading (#8231 D1). */
|
|
98
|
+
library: string;
|
|
99
|
+
artifacts: string;
|
|
100
|
+
/** Shown when a hub sign-out is refused or fails (#8465). */
|
|
101
|
+
signOutFailed: string;
|
|
54
102
|
}
|
|
55
103
|
|
|
56
104
|
export interface AccountFrameProps {
|
|
@@ -61,6 +109,14 @@ export interface AccountFrameProps {
|
|
|
61
109
|
helpItems: NonNullable<SignedInShellProps["helpItems"]>;
|
|
62
110
|
forYou: { items: readonly ForYouItem[]; now: string; labels: ForYouLabels };
|
|
63
111
|
labels: AccountFrameLabels;
|
|
112
|
+
/**
|
|
113
|
+
* Where Log out POSTs on a spoke that signs in through its hub (the seam's
|
|
114
|
+
* `SIGN_OUT_ENDPOINT`, read by the server layout). Absent or `null`: Better
|
|
115
|
+
* Auth's client signs out (#8465).
|
|
116
|
+
*/
|
|
117
|
+
signOutEndpoint?: string | null;
|
|
118
|
+
/** Where Log out lands after a hub sign-out; defaults to the site home. */
|
|
119
|
+
signInHref?: string;
|
|
64
120
|
children: ReactNode;
|
|
65
121
|
}
|
|
66
122
|
|
|
@@ -71,23 +127,61 @@ export function AccountFrame({
|
|
|
71
127
|
helpItems,
|
|
72
128
|
forYou,
|
|
73
129
|
labels,
|
|
130
|
+
signOutEndpoint,
|
|
131
|
+
signInHref,
|
|
74
132
|
children,
|
|
75
133
|
}: AccountFrameProps): ReactElement {
|
|
76
134
|
const router = useRouter();
|
|
77
135
|
const pathname = usePathname();
|
|
78
136
|
const searchParams = useSearchParams();
|
|
79
137
|
const [dismissed] = useForYouDismissed(ACCOUNT_PERSIST_KEY);
|
|
138
|
+
const [topBarActions, setTopBarActions] = useState<HTMLElement | null>(null);
|
|
139
|
+
const [signOutFailed, setSignOutFailed] = useState(false);
|
|
80
140
|
|
|
81
141
|
const homeHref = localeHref(locale, "/");
|
|
82
142
|
const inboxHref = localeHref(locale, "/account");
|
|
143
|
+
const artifactsHref = localeHref(locale, "/account/artifacts");
|
|
144
|
+
const settingsHref = localeHref(locale, "/account/general");
|
|
145
|
+
const within = (href: string): boolean => pathname === href || pathname.startsWith(`${href}/`);
|
|
146
|
+
|
|
147
|
+
// #8430 — a Settings page draws its own standalone frame: the marker only.
|
|
148
|
+
if (isSettingsPath(pathname, locale)) {
|
|
149
|
+
return (
|
|
150
|
+
<div className="contents" data-ailk-shell="account" data-testid="account-shell">
|
|
151
|
+
{children}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
83
156
|
// The Inbox row stays selected on an item's own page (/account/inbox/…).
|
|
84
157
|
const onInbox = pathname === inboxHref || pathname.startsWith(`${inboxHref}/inbox/`);
|
|
158
|
+
const activePath = onInbox ? inboxHref : within(artifactsHref) ? artifactsHref : undefined;
|
|
85
159
|
// #7469 — the phone tab: For you only on the Inbox route itself.
|
|
86
160
|
const phoneTab =
|
|
87
161
|
pathname === inboxHref && searchParams?.get("tab") === FOR_YOU_TAB ? FOR_YOU_TAB : "inbox";
|
|
162
|
+
// #8233 D4 — the Inbox route's New lives in the top bar; nowhere else.
|
|
163
|
+
const onInboxRoute = pathname === inboxHref;
|
|
88
164
|
|
|
89
165
|
const onSignOut = (): void => {
|
|
90
166
|
void (async () => {
|
|
167
|
+
setSignOutFailed(false);
|
|
168
|
+
if (signOutEndpoint) {
|
|
169
|
+
// A refused or failed sign-out leaves the session cookie valid, so the
|
|
170
|
+
// page must not pretend otherwise by navigating away (#8465).
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(signOutEndpoint, { method: "POST", credentials: "same-origin" });
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
setSignOutFailed(true);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
setSignOutFailed(true);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
router.push(signInHref ?? homeHref);
|
|
182
|
+
router.refresh();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
91
185
|
try {
|
|
92
186
|
await authClient.signOut();
|
|
93
187
|
} catch {
|
|
@@ -99,7 +193,16 @@ export function AccountFrame({
|
|
|
99
193
|
})();
|
|
100
194
|
};
|
|
101
195
|
|
|
102
|
-
const {
|
|
196
|
+
const {
|
|
197
|
+
home,
|
|
198
|
+
inbox,
|
|
199
|
+
forYou: forYouLabel,
|
|
200
|
+
settings,
|
|
201
|
+
library,
|
|
202
|
+
artifacts,
|
|
203
|
+
signOutFailed: signOutFailedLabel,
|
|
204
|
+
...shellLabels
|
|
205
|
+
} = labels;
|
|
103
206
|
|
|
104
207
|
// D13 — the logo only: no box, no name, no popover; it links home. The
|
|
105
208
|
// same link is the phone top bar's leading cell.
|
|
@@ -118,52 +221,83 @@ export function AccountFrame({
|
|
|
118
221
|
const forYouLeft = forYou.items.filter((item) => !dismissed.has(item.id));
|
|
119
222
|
|
|
120
223
|
return (
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
224
|
+
<div className="contents" data-ailk-shell="account" data-testid="account-shell">
|
|
225
|
+
<SignedInShell
|
|
226
|
+
persistKey={ACCOUNT_PERSIST_KEY}
|
|
227
|
+
locale={locale}
|
|
228
|
+
navItems={[{ labelKey: "inbox", label: inbox, path: "/account", icon: Inbox }]}
|
|
229
|
+
railGroups={[
|
|
230
|
+
{
|
|
231
|
+
key: "library",
|
|
232
|
+
heading: library,
|
|
233
|
+
items: [
|
|
234
|
+
{ labelKey: "artifacts", label: artifacts, path: "/account/artifacts", icon: Layers },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
]}
|
|
238
|
+
tabItems={[
|
|
239
|
+
{ key: "inbox", label: inbox, href: inboxHref, icon: Inbox, isActive: onInbox && phoneTab === "inbox" },
|
|
240
|
+
{
|
|
241
|
+
key: FOR_YOU_TAB,
|
|
242
|
+
label: forYouLabel,
|
|
243
|
+
href: `${inboxHref}?tab=${FOR_YOU_TAB}`,
|
|
244
|
+
icon: Sparkles,
|
|
245
|
+
isActive: phoneTab === FOR_YOU_TAB,
|
|
246
|
+
},
|
|
247
|
+
]}
|
|
248
|
+
{...(activePath ? { activePath } : {})}
|
|
249
|
+
user={user}
|
|
250
|
+
userMenuItems={[
|
|
251
|
+
{
|
|
252
|
+
id: "settings",
|
|
253
|
+
label: settings,
|
|
254
|
+
icon: Settings,
|
|
255
|
+
href: settingsHref,
|
|
256
|
+
},
|
|
257
|
+
]}
|
|
258
|
+
onSignOut={onSignOut}
|
|
259
|
+
userDisplay="avatar"
|
|
260
|
+
accountMenuLayout="card"
|
|
261
|
+
helpItems={helpItems}
|
|
262
|
+
railHeader={logoLink}
|
|
263
|
+
topBarStart={logoLink}
|
|
264
|
+
{...(onInboxRoute
|
|
265
|
+
? {
|
|
266
|
+
topBarEnd: (
|
|
267
|
+
<span
|
|
268
|
+
ref={setTopBarActions}
|
|
269
|
+
className="flex items-center"
|
|
270
|
+
data-testid="account-top-bar-actions"
|
|
271
|
+
/>
|
|
272
|
+
),
|
|
273
|
+
}
|
|
274
|
+
: {})}
|
|
275
|
+
{...(forYouLeft.length > 0
|
|
276
|
+
? {
|
|
277
|
+
aside: (
|
|
278
|
+
<ForYouPanel
|
|
279
|
+
items={forYouLeft}
|
|
280
|
+
persistKey={ACCOUNT_PERSIST_KEY}
|
|
281
|
+
labels={forYou.labels}
|
|
282
|
+
locale={locale}
|
|
283
|
+
now={forYou.now}
|
|
284
|
+
/>
|
|
285
|
+
),
|
|
286
|
+
}
|
|
287
|
+
: {})}
|
|
288
|
+
labels={shellLabels}
|
|
289
|
+
>
|
|
290
|
+
{signOutFailed ? (
|
|
291
|
+
<InlineMessage variant="error" className="px-4 pt-3" data-testid="account-sign-out-error">
|
|
292
|
+
{signOutFailedLabel}
|
|
293
|
+
</InlineMessage>
|
|
294
|
+
) : null}
|
|
295
|
+
<PhoneTabProvider tab={phoneTab}>
|
|
296
|
+
<NewMenuSlotProvider node={onInboxRoute ? topBarActions : null}>
|
|
297
|
+
{children}
|
|
298
|
+
</NewMenuSlotProvider>
|
|
299
|
+
</PhoneTabProvider>
|
|
300
|
+
</SignedInShell>
|
|
301
|
+
</div>
|
|
168
302
|
);
|
|
169
303
|
}
|
|
@@ -13,62 +13,41 @@
|
|
|
13
13
|
* navigation telemetry for a Server Action, which is inherent to any hosted-
|
|
14
14
|
* portal handoff, not something this design avoids).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Where the portal comes from is `billing-source.ts`'s `billingPortalUrl()`
|
|
17
|
+
* (#8217, spec docs/specs/8217-spoke-billing-spoke-checkout-reads-spec.md
|
|
18
|
+
* D10, D11), the account tree's billing seam — this file tests no topology:
|
|
18
19
|
*
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* (
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* (`lib/hub-session/session.ts`) sends the sealed session's credential as
|
|
29
|
-
* `Authorization: Bearer`, and `authPreHandler` skips its origin check for a
|
|
30
|
-
* bearer. This action does not complete on a spoke: the hub admits a hub
|
|
31
|
-
* credential on four read routes only (`HUB_SESSION_ROUTES`), so
|
|
32
|
-
* `POST /v1/subscription/portal-session` is a 404 there, and Settings hides
|
|
33
|
-
* the Billing button because `subscription.me()` is a 404 too.
|
|
20
|
+
* - A single site: the hub's `POST /v1/subscription/portal-session`, with
|
|
21
|
+
* the caller's cookie and real `Origin` forwarded, which `apps/api`'s
|
|
22
|
+
* `authPreHandler` checks against its trusted origins on this
|
|
23
|
+
* cookie-authenticated mutating request; the route accepts only a cookie
|
|
24
|
+
* session, never a bearer (D3.1/D5).
|
|
25
|
+
* - A spoke that signs in through its hub and carries commerce: the spoke's
|
|
26
|
+
* own Stripe key, for the customer its hub vouches for — read server-side
|
|
27
|
+
* by the rendered billing action, never from this action's input.
|
|
28
|
+
* - A spoke without that action: no portal, so nothing to redirect to.
|
|
34
29
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
30
|
+
* It takes no argument, so nothing a browser sends can name a customer.
|
|
31
|
+
* Next's Server Action origin check applies as before.
|
|
32
|
+
*
|
|
33
|
+
* `redirect()` throws `NEXT_REDIRECT` by design — called OUTSIDE any
|
|
34
|
+
* try/catch (mirrors `checkout.actions.ts`'s idiom) so a catch never
|
|
37
35
|
* swallows the navigation this action exists to perform.
|
|
38
36
|
*/
|
|
39
37
|
|
|
40
|
-
import { subscription } from "@working-theory/api-client";
|
|
41
38
|
import { redirect } from "next/navigation";
|
|
42
39
|
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
/** PII-safe error identity for logs (the `checkout.actions.ts` idiom: `err.name` only). */
|
|
46
|
-
function errorName(err: unknown): string {
|
|
47
|
-
return err instanceof Error ? err.name : "unknown";
|
|
48
|
-
}
|
|
40
|
+
import { billingPortalUrl } from "./billing-source";
|
|
49
41
|
|
|
50
42
|
/**
|
|
51
43
|
* Opens the caller's own Stripe billing-portal session and redirects there.
|
|
52
|
-
* Never returns on success.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
44
|
+
* Never returns on success. With no portal (a failed mint, logged PII-safe by
|
|
45
|
+
* `billingPortalUrl`, or none to open), it returns and the page stays as it
|
|
46
|
+
* is — there is no error UI slot on this button.
|
|
55
47
|
*/
|
|
56
48
|
export async function openBillingPortal(): Promise<void> {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const res = await subscription.portalSession(init);
|
|
62
|
-
if (!res.ok) {
|
|
63
|
-
console.error(`[openBillingPortal] status=${res.status}`);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
url = res.body.url;
|
|
67
|
-
} catch (err) {
|
|
68
|
-
console.error(`[openBillingPortal] errName=${errorName(err)}`);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Outside the try/catch on purpose — see the file header.
|
|
49
|
+
const url = await billingPortalUrl();
|
|
50
|
+
if (url === null) return;
|
|
51
|
+
// Outside any try/catch on purpose — see the file header.
|
|
73
52
|
redirect(url);
|
|
74
53
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /[locale]/account/artifacts/[pieceId] — one artifact (#8234, spec
|
|
3
|
+
* docs/specs/8234-member-artifacts-account-artifacts-and-spec.md D4).
|
|
4
|
+
*
|
|
5
|
+
* RSC, inside the account layout's SignedInShell, through the shared
|
|
6
|
+
* `InboxItemFrame`: back to Artifacts, the title, the review state, and —
|
|
7
|
+
* once approved — "Approved <date>". The body lists every served version in
|
|
8
|
+
* the read's order, newest first: its number, its delivery date, its note,
|
|
9
|
+
* the member's own review when there is one, and one action — Download for a
|
|
10
|
+
* file (re-served by `versions/[version]/file/route.ts`, never shown inline
|
|
11
|
+
* on this origin) or View for an https link, opened in a new tab. A link
|
|
12
|
+
* that is not https renders no action. The read serves no deliverer (8169
|
|
13
|
+
* D6), so none is shown.
|
|
14
|
+
*
|
|
15
|
+
* Inside the latest version only: the review form while it is reviewable
|
|
16
|
+
* (not approved, no review yet — `isReviewable`), else the waiting line once
|
|
17
|
+
* the member asked for changes; nothing once approved. Nothing limits how
|
|
18
|
+
* often: every new latest version is reviewable again (#8140 disposition 3).
|
|
19
|
+
*
|
|
20
|
+
* The artifact is found the way the Inbox item page finds its item: the
|
|
21
|
+
* artifacts read runs (deduped per request) and `[pieceId]` is matched raw or
|
|
22
|
+
* decoded. No session, no match or a 404 read is a not-found; any other
|
|
23
|
+
* failed read renders the error row with Retry (spec 7423 D12). Titles,
|
|
24
|
+
* notes and comments render as text only.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { InboxItemFrame } from "@working-theory/templates/account";
|
|
28
|
+
import { localeHref } from "@working-theory/ui";
|
|
29
|
+
import { Button, Heading, Link, Text } from "@working-theory/ui/primitives";
|
|
30
|
+
import {
|
|
31
|
+
isHttpsLink,
|
|
32
|
+
type MemberArtifact,
|
|
33
|
+
type MemberPieceVersion,
|
|
34
|
+
} from "@working-theory/validation";
|
|
35
|
+
import type { Metadata } from "next";
|
|
36
|
+
import { notFound } from "next/navigation";
|
|
37
|
+
import { getTranslations } from "next-intl/server";
|
|
38
|
+
import type { ReactElement } from "react";
|
|
39
|
+
|
|
40
|
+
import { accountSession, logReadFailure } from "../../account-data";
|
|
41
|
+
import { ErrorRow } from "../../error-row";
|
|
42
|
+
import {
|
|
43
|
+
artifactState,
|
|
44
|
+
isReviewable,
|
|
45
|
+
loadArtifacts,
|
|
46
|
+
STATE_KEYS,
|
|
47
|
+
versionFileHref,
|
|
48
|
+
} from "../artifact-data";
|
|
49
|
+
|
|
50
|
+
import { ReviewForm, type ReviewFormLabels } from "./review-form";
|
|
51
|
+
|
|
52
|
+
interface Props {
|
|
53
|
+
params: Promise<{ locale: string; pieceId: string }>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A segment, percent-decoded when it decodes — matched in both forms, so an
|
|
58
|
+
* id is found whether or not the framework already decoded it.
|
|
59
|
+
*/
|
|
60
|
+
function decodeSegment(segment: string): string {
|
|
61
|
+
try {
|
|
62
|
+
return decodeURIComponent(segment);
|
|
63
|
+
} catch {
|
|
64
|
+
return segment;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
69
|
+
const { locale } = await params;
|
|
70
|
+
const t = await getTranslations({ locale, namespace: "account" });
|
|
71
|
+
return { title: t("artifacts.title") };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default async function AccountArtifactPage({
|
|
75
|
+
params,
|
|
76
|
+
}: Props): Promise<ReactElement> {
|
|
77
|
+
const { locale, pieceId: segment } = await params;
|
|
78
|
+
const t = await getTranslations({ locale, namespace: "account" });
|
|
79
|
+
const session = await accountSession(locale);
|
|
80
|
+
if (!session) notFound();
|
|
81
|
+
|
|
82
|
+
let artifacts: MemberArtifact[];
|
|
83
|
+
try {
|
|
84
|
+
artifacts = await loadArtifacts(session.ctx);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
logReadFailure("artifacts", err);
|
|
87
|
+
return (
|
|
88
|
+
<div className="max-w-3xl py-8" data-testid="artifact">
|
|
89
|
+
<ErrorRow
|
|
90
|
+
message={t("artifacts.itemError")}
|
|
91
|
+
retryLabel={t("artifacts.retry")}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const artifact = artifacts.find(
|
|
98
|
+
(candidate) =>
|
|
99
|
+
candidate.id === segment || candidate.id === decodeSegment(segment),
|
|
100
|
+
);
|
|
101
|
+
if (!artifact) notFound();
|
|
102
|
+
|
|
103
|
+
const date = (iso: string): string =>
|
|
104
|
+
new Intl.DateTimeFormat(locale, {
|
|
105
|
+
dateStyle: "medium",
|
|
106
|
+
timeZone: "UTC",
|
|
107
|
+
}).format(new Date(iso));
|
|
108
|
+
const state = artifactState(artifact);
|
|
109
|
+
const reviewable = isReviewable(artifact);
|
|
110
|
+
const latest = artifact.versions[0]!.version;
|
|
111
|
+
const labels: ReviewFormLabels = {
|
|
112
|
+
heading: t("artifacts.review.heading", { version: latest }),
|
|
113
|
+
comment: t("artifacts.review.comment"),
|
|
114
|
+
commentHint: t("artifacts.review.commentHint"),
|
|
115
|
+
approve: t("artifacts.review.approve"),
|
|
116
|
+
requestChanges: t("artifacts.review.requestChanges"),
|
|
117
|
+
submitting: t("artifacts.review.submitting"),
|
|
118
|
+
sent: t("artifacts.review.sent"),
|
|
119
|
+
errors: {
|
|
120
|
+
comment_required: t("artifacts.review.errors.commentRequired"),
|
|
121
|
+
closed: t("artifacts.review.errors.closed"),
|
|
122
|
+
not_latest: t("artifacts.review.errors.notLatest"),
|
|
123
|
+
rate_limited: t("artifacts.review.errors.rateLimited"),
|
|
124
|
+
failed: t("artifacts.review.errors.failed"),
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// The one action of a version: Download a file, View an https link, else none.
|
|
129
|
+
const versionAction = (v: MemberPieceVersion): ReactElement | null => {
|
|
130
|
+
if (v.file !== null) {
|
|
131
|
+
return (
|
|
132
|
+
<Button asChild variant="outline" className="max-md:min-h-11">
|
|
133
|
+
<Link
|
|
134
|
+
href={versionFileHref(locale, artifact.id, v.version)}
|
|
135
|
+
variant="bare"
|
|
136
|
+
data-testid={`artifact-version-${v.version}-download`}
|
|
137
|
+
>
|
|
138
|
+
{t("artifacts.download", { filename: v.file.filename })}
|
|
139
|
+
</Link>
|
|
140
|
+
</Button>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (isHttpsLink(v.link)) {
|
|
144
|
+
return (
|
|
145
|
+
<Button asChild variant="outline" className="max-md:min-h-11">
|
|
146
|
+
<Link
|
|
147
|
+
href={v.link}
|
|
148
|
+
external
|
|
149
|
+
variant="bare"
|
|
150
|
+
data-testid={`artifact-version-${v.version}-view`}
|
|
151
|
+
>
|
|
152
|
+
{t("artifacts.view")}
|
|
153
|
+
</Link>
|
|
154
|
+
</Button>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<InboxItemFrame
|
|
162
|
+
back={{
|
|
163
|
+
href: localeHref(locale, "/account/artifacts"),
|
|
164
|
+
label: t("artifacts.back"),
|
|
165
|
+
}}
|
|
166
|
+
caption={t("artifacts.caption")}
|
|
167
|
+
title={artifact.title}
|
|
168
|
+
status={t(STATE_KEYS[state])}
|
|
169
|
+
lead={
|
|
170
|
+
artifact.approvedAt !== null
|
|
171
|
+
? t("artifacts.approvedOn", { date: date(artifact.approvedAt) })
|
|
172
|
+
: null
|
|
173
|
+
}
|
|
174
|
+
testIdPrefix="artifact"
|
|
175
|
+
>
|
|
176
|
+
<section className="mt-block-loose">
|
|
177
|
+
<Heading level={2} visual={4} className="!text-body font-semibold">
|
|
178
|
+
{t("artifacts.versions")}
|
|
179
|
+
</Heading>
|
|
180
|
+
<div className="mt-block-tight flex flex-col gap-4">
|
|
181
|
+
{artifact.versions.map((v) => {
|
|
182
|
+
const action = versionAction(v);
|
|
183
|
+
return (
|
|
184
|
+
<div
|
|
185
|
+
key={v.version}
|
|
186
|
+
className="rounded-md border-hairline border-(--color-border) p-card"
|
|
187
|
+
data-testid={`artifact-version-${v.version}`}
|
|
188
|
+
>
|
|
189
|
+
<Text as="p" variant="body" className="font-semibold">
|
|
190
|
+
{t("artifacts.version", { version: v.version })}
|
|
191
|
+
</Text>
|
|
192
|
+
<Text as="p" variant="body-sm" tone="muted">
|
|
193
|
+
{t("artifacts.deliveredOn", { date: date(v.deliveredAt) })}
|
|
194
|
+
</Text>
|
|
195
|
+
{v.note !== null ? (
|
|
196
|
+
<Text
|
|
197
|
+
as="p"
|
|
198
|
+
variant="body-sm"
|
|
199
|
+
className="mt-2 whitespace-pre-line"
|
|
200
|
+
>
|
|
201
|
+
{v.note}
|
|
202
|
+
</Text>
|
|
203
|
+
) : null}
|
|
204
|
+
{v.review !== null && v.reviewedAt !== null ? (
|
|
205
|
+
<div className="mt-2">
|
|
206
|
+
<Text as="p" variant="body-sm" tone="muted">
|
|
207
|
+
{v.review === "approved"
|
|
208
|
+
? t("artifacts.reviewApproved", {
|
|
209
|
+
date: date(v.reviewedAt),
|
|
210
|
+
})
|
|
211
|
+
: t("artifacts.reviewChangesRequested", {
|
|
212
|
+
date: date(v.reviewedAt),
|
|
213
|
+
})}
|
|
214
|
+
</Text>
|
|
215
|
+
{v.reviewComment !== null ? (
|
|
216
|
+
<Text
|
|
217
|
+
as="p"
|
|
218
|
+
variant="body-sm"
|
|
219
|
+
className="whitespace-pre-line"
|
|
220
|
+
>
|
|
221
|
+
{v.reviewComment}
|
|
222
|
+
</Text>
|
|
223
|
+
) : null}
|
|
224
|
+
</div>
|
|
225
|
+
) : null}
|
|
226
|
+
{action !== null ? (
|
|
227
|
+
<div className="mt-block-tight">{action}</div>
|
|
228
|
+
) : null}
|
|
229
|
+
{v.version === latest && reviewable ? (
|
|
230
|
+
<ReviewForm
|
|
231
|
+
pieceId={artifact.id}
|
|
232
|
+
version={v.version}
|
|
233
|
+
labels={labels}
|
|
234
|
+
/>
|
|
235
|
+
) : null}
|
|
236
|
+
{v.version === latest && state === "changes-requested" ? (
|
|
237
|
+
<div
|
|
238
|
+
className="mt-block-tight"
|
|
239
|
+
data-testid="artifact-review-waiting"
|
|
240
|
+
>
|
|
241
|
+
<Text as="p" variant="body-sm">
|
|
242
|
+
{t("artifacts.review.waiting")}
|
|
243
|
+
</Text>
|
|
244
|
+
</div>
|
|
245
|
+
) : null}
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
})}
|
|
249
|
+
</div>
|
|
250
|
+
</section>
|
|
251
|
+
</InboxItemFrame>
|
|
252
|
+
);
|
|
253
|
+
}
|