@forumone/create-throughline 0.0.1 → 0.2.0
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +41 -43
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +45 -0
- package/dist/bin.js.map +1 -0
- package/dist/generator.d.ts +48 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +131 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/post-install.d.ts +9 -0
- package/dist/post-install.d.ts.map +1 -0
- package/dist/post-install.js +68 -0
- package/dist/post-install.js.map +1 -0
- package/dist/prompts.d.ts +23 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +89 -0
- package/dist/prompts.js.map +1 -0
- package/dist/templates/base/.env.example +43 -0
- package/dist/templates/base/README.md +96 -0
- package/dist/templates/base/apps/web/next-env.d.ts +5 -0
- package/dist/templates/base/apps/web/next.config.mjs +10 -0
- package/dist/templates/base/apps/web/package.json.template +42 -0
- package/dist/templates/base/apps/web/src/app/(frontend)/layout.tsx +22 -0
- package/dist/templates/base/apps/web/src/app/(frontend)/page.tsx +18 -0
- package/dist/templates/base/apps/web/src/app/(payload)/admin/[[...segments]]/not-found.tsx +12 -0
- package/dist/templates/base/apps/web/src/app/(payload)/admin/[[...segments]]/page.tsx +16 -0
- package/dist/templates/base/apps/web/src/app/(payload)/admin/importMap.js +6 -0
- package/dist/templates/base/apps/web/src/app/(payload)/api/[...slug]/route.ts +16 -0
- package/dist/templates/base/apps/web/src/app/(payload)/layout.tsx +23 -0
- package/dist/templates/base/apps/web/src/app/api/inngest/route.ts.template +56 -0
- package/dist/templates/base/apps/web/src/payload.config.ts.template +164 -0
- package/dist/templates/base/apps/web/tsconfig.json +24 -0
- package/dist/templates/base/package.json.template +23 -0
- package/dist/templates/base/pnpm-workspace.yaml +3 -0
- package/dist/templates/base/tsconfig.json +14 -0
- package/dist/templates/base/turbo.json +27 -0
- package/dist/templates/with-reference-ds/packages/design-system/package.json.template +18 -0
- package/dist/templates/with-reference-ds/packages/design-system/src/index.ts +6 -0
- package/dist/templates/with-reference-ds/packages/design-system/src/manifest.ts +6 -0
- package/dist/templates/with-reference-ds/packages/design-system/tsconfig.json +9 -0
- package/dist/templates/without-reference-ds/packages/design-system/README.md +19 -0
- package/dist/templates/without-reference-ds/packages/design-system/package.json.template +14 -0
- package/dist/templates/without-reference-ds/packages/design-system/src/index.ts +8 -0
- package/dist/utils/templates.d.ts +11 -0
- package/dist/utils/templates.d.ts.map +1 -0
- package/dist/utils/templates.js +31 -0
- package/dist/utils/templates.js.map +1 -0
- package/package.json +64 -7
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# --- Required ---
|
|
2
|
+
DATABASE_URI=postgres://user:pass@host/dbname
|
|
3
|
+
PAYLOAD_SECRET=generate-with-openssl-rand-base64-48
|
|
4
|
+
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
|
|
5
|
+
|
|
6
|
+
# --- Inngest ---
|
|
7
|
+
# Leave blank in local dev; use the inngest dev server (npx inngest-cli dev).
|
|
8
|
+
# Set both in production after creating an Inngest app.
|
|
9
|
+
INNGEST_EVENT_KEY=
|
|
10
|
+
INNGEST_SIGNING_KEY=
|
|
11
|
+
|
|
12
|
+
# --- Resend (for approval + form notification emails) ---
|
|
13
|
+
RESEND_API_KEY=
|
|
14
|
+
EMAIL_FROM_ADDRESS=notifications@example.com
|
|
15
|
+
EMAIL_FROM_NAME={{projectName}}
|
|
16
|
+
EMAIL_REPLY_TO=
|
|
17
|
+
|
|
18
|
+
# --- Approval tokens ---
|
|
19
|
+
# Generate: openssl rand -base64 48
|
|
20
|
+
APPROVAL_TOKEN_SECRET=
|
|
21
|
+
|
|
22
|
+
# --- Forms ---
|
|
23
|
+
# Generate: openssl rand -base64 48
|
|
24
|
+
FORMS_IP_HASH_SECRET=
|
|
25
|
+
|
|
26
|
+
# --- MCP server keys ---
|
|
27
|
+
# Each key is generated in the Payload admin (API Keys collection) after
|
|
28
|
+
# first deploy and pasted back here for local dev.
|
|
29
|
+
COMPONENT_SERVER_API_KEY=
|
|
30
|
+
PUBLISHING_SERVER_API_KEY=
|
|
31
|
+
APPROVALS_SERVER_API_KEY=
|
|
32
|
+
AUDIT_SERVER_API_KEY=
|
|
33
|
+
FORMS_SERVER_API_KEY=
|
|
34
|
+
INTEGRATIONS_SERVER_API_KEY=
|
|
35
|
+
|
|
36
|
+
# System API key used by scheduled workflows that talk back to the
|
|
37
|
+
# Publishing MCP server (executes scheduled publishes).
|
|
38
|
+
PUBLISHING_SYSTEM_API_KEY=
|
|
39
|
+
|
|
40
|
+
# --- Optional: storage ---
|
|
41
|
+
# Vercel Blob token if deploying to Vercel; remove this and the storage
|
|
42
|
+
# adapter from payload.config.ts if you're using a different provider.
|
|
43
|
+
BLOB_READ_WRITE_TOKEN=
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
Generated by [`@forumone/create-throughline`](https://github.com/forumone/throughline/tree/main/packages/create-throughline).
|
|
4
|
+
|
|
5
|
+
This is a Throughline project: a Payload CMS application wired with the Throughline plugin suite (audit, components, publishing, approvals, audit-query, integrations, email, forms) plus an Inngest endpoint that runs the workflow functions.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- Node.js >= 20.9
|
|
10
|
+
- pnpm >= 10
|
|
11
|
+
- A Postgres database (Neon / Supabase / self-hosted)
|
|
12
|
+
- An [Inngest](https://inngest.com) account (free tier is fine for development)
|
|
13
|
+
- A [Resend](https://resend.com) API key for transactional email
|
|
14
|
+
|
|
15
|
+
## First-time setup
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# 1. Install deps (skip if you let the scaffolder do it)
|
|
19
|
+
pnpm install
|
|
20
|
+
|
|
21
|
+
# 2. Copy the env template
|
|
22
|
+
cp .env.example .env.local
|
|
23
|
+
|
|
24
|
+
# 3. Generate secrets (run each three times for the three secret slots)
|
|
25
|
+
openssl rand -base64 48
|
|
26
|
+
|
|
27
|
+
# 4. Edit .env.local and fill in:
|
|
28
|
+
# - DATABASE_URI — your Postgres connection string
|
|
29
|
+
# - PAYLOAD_SECRET — generated above
|
|
30
|
+
# - APPROVAL_TOKEN_SECRET — generated above
|
|
31
|
+
# - FORMS_IP_HASH_SECRET — generated above
|
|
32
|
+
# - RESEND_API_KEY — from Resend dashboard
|
|
33
|
+
# - INNGEST_EVENT_KEY/SIGNING — from Inngest (optional in local dev)
|
|
34
|
+
|
|
35
|
+
# 5. Start the dev server
|
|
36
|
+
pnpm dev
|
|
37
|
+
|
|
38
|
+
# 6. Visit http://localhost:3000/admin and create your first user.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## After admin signup
|
|
42
|
+
|
|
43
|
+
1. In the Payload admin, open the **API Keys** collection and create one key per MCP server (Component, Publishing, Approvals, Audit, Forms, Integrations). Paste each key into `.env.local` (`COMPONENT_SERVER_API_KEY`, etc.) and restart `pnpm dev`.
|
|
44
|
+
|
|
45
|
+
2. **Plug the MCP servers into Claude (or your MCP client).** The endpoints are:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
/api/components/mcp
|
|
49
|
+
/api/publishing/mcp
|
|
50
|
+
/api/approvals/mcp
|
|
51
|
+
/api/audit/mcp
|
|
52
|
+
/api/forms/mcp
|
|
53
|
+
/api/integrations/mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Authenticate with the corresponding API key from `.env.local`.
|
|
57
|
+
|
|
58
|
+
3. **Replace the example `pages` collection** in `apps/web/src/payload.config.ts` with your real content model. Keep the `policy` group on collections that should require approval — that field shape is what the approvals plugin reads.
|
|
59
|
+
|
|
60
|
+
4. **Implement the user/group resolvers** in `payload.config.ts`. The scaffold ships with stub functions that return empty arrays so the app compiles; the approvals plugin won't actually work until they're real.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
- `pnpm dev` — start Next.js + Payload in dev mode
|
|
65
|
+
- `pnpm build` — production build
|
|
66
|
+
- `pnpm typecheck` — TypeScript check across the monorepo
|
|
67
|
+
- `pnpm lint` — ESLint
|
|
68
|
+
- `pnpm test` — run all package tests
|
|
69
|
+
|
|
70
|
+
## Project layout
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
{{projectName}}/
|
|
74
|
+
├── apps/
|
|
75
|
+
│ └── web/ # Next.js + Payload host
|
|
76
|
+
├── packages/
|
|
77
|
+
│ ├── design-system/ # UI components consumed by Components MCP
|
|
78
|
+
│ ├── content/ # Reusable collections + blocks (optional)
|
|
79
|
+
│ └── brand/ # Brand tokens / email theme (optional)
|
|
80
|
+
├── .env.example
|
|
81
|
+
├── pnpm-workspace.yaml
|
|
82
|
+
├── turbo.json
|
|
83
|
+
└── tsconfig.json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Documentation
|
|
87
|
+
|
|
88
|
+
Throughline docs: https://github.com/forumone/throughline
|
|
89
|
+
|
|
90
|
+
Payload docs: https://payloadcms.com/docs
|
|
91
|
+
|
|
92
|
+
Inngest docs: https://www.inngest.com/docs
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
UNLICENSED — generated project. Add your own license file before publishing.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{#if packageScope}}@{{packageScope}}/web{{else}}{{projectName}}-web{{/if}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"clean": "rm -rf .next .turbo",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"payload": "payload"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@forumone/throughline-approvals": "^0.2.0",
|
|
16
|
+
"@forumone/throughline-audit": "^0.2.0",
|
|
17
|
+
"@forumone/throughline-components": "^0.2.0",
|
|
18
|
+
"@forumone/throughline-core": "^0.2.0",
|
|
19
|
+
"@forumone/throughline-email": "^0.2.0",
|
|
20
|
+
"@forumone/throughline-forms": "^0.2.0",
|
|
21
|
+
"@forumone/throughline-integrations": "^0.2.0",
|
|
22
|
+
"@forumone/throughline-publishing": "^0.2.0",
|
|
23
|
+
"@forumone/throughline-workflows": "^0.2.0",
|
|
24
|
+
{{#if useReferenceDs}}"@forumone/throughline-reference-ds": "^0.1.0",
|
|
25
|
+
{{/if}}"@payloadcms/db-postgres": "^3.83.0",
|
|
26
|
+
"@payloadcms/next": "^3.83.0",
|
|
27
|
+
"@payloadcms/plugin-form-builder": "3.83.0",
|
|
28
|
+
"@payloadcms/richtext-lexical": "^3.83.0",
|
|
29
|
+
"graphql": "^16.8.1",
|
|
30
|
+
"inngest": "^4.2.0",
|
|
31
|
+
"next": "^16.2.4",
|
|
32
|
+
"payload": "^3.83.0",
|
|
33
|
+
"react": "^19.2.0",
|
|
34
|
+
"react-dom": "^19.2.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.17.0",
|
|
38
|
+
"@types/react": "^19.2.0",
|
|
39
|
+
"@types/react-dom": "^19.2.0",
|
|
40
|
+
"typescript": "^5.6.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: '{{projectName}}',
|
|
5
|
+
description: 'Powered by Payload CMS + Throughline',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function FrontendLayout({ children }: { children: ReactNode }) {
|
|
9
|
+
return (
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<body
|
|
12
|
+
style={{
|
|
13
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
14
|
+
margin: 0,
|
|
15
|
+
padding: 0,
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default function HomePage() {
|
|
2
|
+
return (
|
|
3
|
+
<main style={{ padding: '4rem 2rem', maxWidth: 720, margin: '0 auto' }}>
|
|
4
|
+
<h1>{{projectName}}</h1>
|
|
5
|
+
<p>
|
|
6
|
+
This is a placeholder home page generated by{' '}
|
|
7
|
+
<code>@forumone/create-throughline</code>.
|
|
8
|
+
</p>
|
|
9
|
+
<p>
|
|
10
|
+
Visit{' '}
|
|
11
|
+
<a href="/admin">
|
|
12
|
+
<code>/admin</code>
|
|
13
|
+
</a>{' '}
|
|
14
|
+
to set up your first user, then replace this page with your real frontend.
|
|
15
|
+
</p>
|
|
16
|
+
</main>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import configPromise from '@payload-config'
|
|
2
|
+
import { NotFoundPage } from '@payloadcms/next/views'
|
|
3
|
+
import { importMap } from '../importMap.js'
|
|
4
|
+
|
|
5
|
+
type Args = {
|
|
6
|
+
params: Promise<{ segments: string[] }>
|
|
7
|
+
searchParams: Promise<Record<string, string | string[]>>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function NotFound({ params, searchParams }: Args) {
|
|
11
|
+
return NotFoundPage({ config: configPromise, importMap, params, searchParams })
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import configPromise from '@payload-config'
|
|
3
|
+
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
|
|
4
|
+
import { importMap } from '../importMap.js'
|
|
5
|
+
|
|
6
|
+
type Args = {
|
|
7
|
+
params: Promise<{ segments: string[] }>
|
|
8
|
+
searchParams: Promise<Record<string, string | string[]>>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
|
12
|
+
generatePageMetadata({ config: configPromise, params, searchParams })
|
|
13
|
+
|
|
14
|
+
export default function Page({ params, searchParams }: Args) {
|
|
15
|
+
return RootPage({ config: configPromise, importMap, params, searchParams })
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// This file is regenerated by `pnpm payload generate:importmap`. The
|
|
2
|
+
// initial export below is a stub so the app compiles before the first
|
|
3
|
+
// generation; payload will rewrite it on first run / when richtext
|
|
4
|
+
// features are added.
|
|
5
|
+
/** @type import('payload').ImportMap */
|
|
6
|
+
export const importMap = {}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import configPromise from '@payload-config'
|
|
2
|
+
import {
|
|
3
|
+
REST_DELETE,
|
|
4
|
+
REST_GET,
|
|
5
|
+
REST_OPTIONS,
|
|
6
|
+
REST_PATCH,
|
|
7
|
+
REST_POST,
|
|
8
|
+
REST_PUT,
|
|
9
|
+
} from '@payloadcms/next/routes'
|
|
10
|
+
|
|
11
|
+
export const DELETE = REST_DELETE(configPromise)
|
|
12
|
+
export const GET = REST_GET(configPromise)
|
|
13
|
+
export const OPTIONS = REST_OPTIONS(configPromise)
|
|
14
|
+
export const PATCH = REST_PATCH(configPromise)
|
|
15
|
+
export const POST = REST_POST(configPromise)
|
|
16
|
+
export const PUT = REST_PUT(configPromise)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ServerFunctionClient } from 'payload'
|
|
2
|
+
import configPromise from '@payload-config'
|
|
3
|
+
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { importMap } from './admin/importMap.js'
|
|
6
|
+
import '@payloadcms/next/css'
|
|
7
|
+
|
|
8
|
+
const serverFunction: ServerFunctionClient = async function (args) {
|
|
9
|
+
'use server'
|
|
10
|
+
return handleServerFunctions({
|
|
11
|
+
...args,
|
|
12
|
+
config: configPromise,
|
|
13
|
+
importMap,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
18
|
+
return (
|
|
19
|
+
<RootLayout config={configPromise} importMap={importMap} serverFunction={serverFunction}>
|
|
20
|
+
{children}
|
|
21
|
+
</RootLayout>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { serve } from 'inngest/next'
|
|
2
|
+
import { getPayload } from 'payload'
|
|
3
|
+
import config from '@/payload.config'
|
|
4
|
+
import { createInngestClient } from '@forumone/throughline-core'
|
|
5
|
+
import {
|
|
6
|
+
createAuditEventEchoFunction,
|
|
7
|
+
createExecuteScheduledPublishesFunction,
|
|
8
|
+
createExpireStaleApprovalsFunction,
|
|
9
|
+
createHealthcheckFunction,
|
|
10
|
+
createPayloadReachableCheck,
|
|
11
|
+
createRevalidateOnPublishFunction,
|
|
12
|
+
} from '@forumone/throughline-workflows'
|
|
13
|
+
import { getEmailFunctions } from '@forumone/throughline-email'
|
|
14
|
+
import { getFormsFunctions } from '@forumone/throughline-forms'
|
|
15
|
+
import {
|
|
16
|
+
getIntegrationContext,
|
|
17
|
+
getIntegrationRegistry,
|
|
18
|
+
} from '@forumone/throughline-integrations'
|
|
19
|
+
|
|
20
|
+
const inngest = createInngestClient({ id: '{{projectName}}' })
|
|
21
|
+
const payload = await getPayload({ config })
|
|
22
|
+
|
|
23
|
+
const emailFunctions = getEmailFunctions(payload) ?? []
|
|
24
|
+
const formsFunctions = getFormsFunctions(payload) ?? []
|
|
25
|
+
const integrationRegistry = getIntegrationRegistry(payload)
|
|
26
|
+
const integrationContext = getIntegrationContext(payload)
|
|
27
|
+
const integrationFunctions =
|
|
28
|
+
integrationRegistry && integrationContext
|
|
29
|
+
? integrationRegistry
|
|
30
|
+
.list()
|
|
31
|
+
.flatMap((integration) => integration.createFunctions(integrationContext))
|
|
32
|
+
: []
|
|
33
|
+
|
|
34
|
+
export const { GET, POST, PUT } = serve({
|
|
35
|
+
client: inngest,
|
|
36
|
+
functions: [
|
|
37
|
+
createRevalidateOnPublishFunction({ inngest, payload }),
|
|
38
|
+
createExecuteScheduledPublishesFunction({
|
|
39
|
+
inngest,
|
|
40
|
+
payload,
|
|
41
|
+
collections: [{ slug: 'pages' }],
|
|
42
|
+
publishingServerUrl: process.env.NEXT_PUBLIC_SERVER_URL!,
|
|
43
|
+
systemApiKey: process.env.PUBLISHING_SYSTEM_API_KEY!,
|
|
44
|
+
}),
|
|
45
|
+
createExpireStaleApprovalsFunction({ inngest, payload }),
|
|
46
|
+
createAuditEventEchoFunction({ inngest }),
|
|
47
|
+
createHealthcheckFunction({
|
|
48
|
+
inngest,
|
|
49
|
+
payload,
|
|
50
|
+
checks: [createPayloadReachableCheck({ payload })],
|
|
51
|
+
}),
|
|
52
|
+
...emailFunctions,
|
|
53
|
+
...formsFunctions,
|
|
54
|
+
...integrationFunctions,
|
|
55
|
+
],
|
|
56
|
+
})
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { buildConfig } from 'payload'
|
|
4
|
+
import type { CollectionConfig } from 'payload'
|
|
5
|
+
import { postgresAdapter } from '@payloadcms/db-postgres'
|
|
6
|
+
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
|
7
|
+
import {
|
|
8
|
+
auditPlugin,
|
|
9
|
+
createApiKeysCollection,
|
|
10
|
+
createInngestClient,
|
|
11
|
+
} from '@forumone/throughline-core'
|
|
12
|
+
import { componentsPlugin } from '@forumone/throughline-components'
|
|
13
|
+
import { publishingPlugin } from '@forumone/throughline-publishing'
|
|
14
|
+
import { approvalsPlugin } from '@forumone/throughline-approvals'
|
|
15
|
+
import { auditQueryPlugin } from '@forumone/throughline-audit'
|
|
16
|
+
import { integrationsPlugin } from '@forumone/throughline-integrations'
|
|
17
|
+
import { emailPlugin } from '@forumone/throughline-email'
|
|
18
|
+
import { formsPlugin } from '@forumone/throughline-forms'
|
|
19
|
+
{{#if useReferenceDs}}import referenceManifest from '@forumone/throughline-reference-ds/manifest' with { type: 'json' }
|
|
20
|
+
{{/if}}
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
23
|
+
const __dirname = path.dirname(__filename)
|
|
24
|
+
|
|
25
|
+
const inngest = createInngestClient({ id: '{{projectName}}' })
|
|
26
|
+
|
|
27
|
+
// Users collection — extend with your own fields and SSO/auth provider.
|
|
28
|
+
const Users: CollectionConfig = {
|
|
29
|
+
slug: 'users',
|
|
30
|
+
auth: true,
|
|
31
|
+
admin: { useAsTitle: 'email' },
|
|
32
|
+
fields: [
|
|
33
|
+
{ name: 'name', type: 'text' },
|
|
34
|
+
{
|
|
35
|
+
name: 'roles',
|
|
36
|
+
type: 'select',
|
|
37
|
+
hasMany: true,
|
|
38
|
+
defaultValue: ['admin'],
|
|
39
|
+
options: ['admin', 'editor', 'approver', 'form-admin'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'groups',
|
|
43
|
+
type: 'select',
|
|
44
|
+
hasMany: true,
|
|
45
|
+
options: ['editorial', 'legal', 'communications', 'senior'],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Example Pages collection. Replace this with your real content model;
|
|
51
|
+
// keep the `policy` group on collections that should require approval —
|
|
52
|
+
// the approvals plugin reads that field shape.
|
|
53
|
+
const Pages: CollectionConfig = {
|
|
54
|
+
slug: 'pages',
|
|
55
|
+
admin: { useAsTitle: 'title' },
|
|
56
|
+
versions: { drafts: true },
|
|
57
|
+
fields: [
|
|
58
|
+
{ name: 'title', type: 'text', required: true },
|
|
59
|
+
{ name: 'slug', type: 'text', required: true, unique: true },
|
|
60
|
+
{
|
|
61
|
+
name: 'seo',
|
|
62
|
+
type: 'group',
|
|
63
|
+
fields: [
|
|
64
|
+
{ name: 'title', type: 'text' },
|
|
65
|
+
{ name: 'description', type: 'textarea' },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'policy',
|
|
70
|
+
type: 'group',
|
|
71
|
+
fields: [
|
|
72
|
+
{ name: 'requiresApproval', type: 'checkbox', defaultValue: false },
|
|
73
|
+
{
|
|
74
|
+
name: 'approverGroups',
|
|
75
|
+
type: 'select',
|
|
76
|
+
hasMany: true,
|
|
77
|
+
options: ['editorial', 'legal', 'communications', 'senior'],
|
|
78
|
+
},
|
|
79
|
+
{ name: 'embargoedUntil', type: 'date' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{ name: 'publishedAt', type: 'date' },
|
|
83
|
+
{ name: 'scheduledPublishAt', type: 'date' },
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default buildConfig({
|
|
88
|
+
admin: { user: 'users' },
|
|
89
|
+
editor: lexicalEditor(),
|
|
90
|
+
db: postgresAdapter({
|
|
91
|
+
pool: { connectionString: process.env.DATABASE_URI! },
|
|
92
|
+
}),
|
|
93
|
+
collections: [Users, Pages, createApiKeysCollection()],
|
|
94
|
+
secret: process.env.PAYLOAD_SECRET!,
|
|
95
|
+
typescript: {
|
|
96
|
+
outputFile: path.resolve(__dirname, 'payload-types.ts'),
|
|
97
|
+
},
|
|
98
|
+
plugins: [
|
|
99
|
+
// Order matters: audit first (so other plugins can write to it),
|
|
100
|
+
// then content plugins, then notification/forms/integrations.
|
|
101
|
+
auditPlugin({ inngest }),
|
|
102
|
+
|
|
103
|
+
componentsPlugin({
|
|
104
|
+
{{#if useReferenceDs}}manifest: { type: 'object', manifest: referenceManifest },
|
|
105
|
+
{{else}}// TODO: point at your design system manifest
|
|
106
|
+
manifest: { type: 'url', url: 'https://your-design-system.example.com/manifest.json' },
|
|
107
|
+
{{/if}}matching: { strategy: 'tfidf' },
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
publishingPlugin({
|
|
111
|
+
collections: [{ slug: 'pages' }],
|
|
112
|
+
inngest,
|
|
113
|
+
// Lazy-resolve approvals at call time so plugin order doesn't matter.
|
|
114
|
+
approvalResolver: {
|
|
115
|
+
getActiveApproval: async () => null, // TODO: wire to approvals plugin
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
|
|
119
|
+
approvalsPlugin({
|
|
120
|
+
inngest,
|
|
121
|
+
groups: [
|
|
122
|
+
{ slug: 'editorial', name: 'Editorial' },
|
|
123
|
+
{ slug: 'legal', name: 'Legal' },
|
|
124
|
+
{ slug: 'communications', name: 'Communications' },
|
|
125
|
+
{ slug: 'senior', name: 'Senior leadership' },
|
|
126
|
+
],
|
|
127
|
+
groupResolver: {
|
|
128
|
+
// TODO: replace with your actual user/group lookup (DB query, SSO, etc.)
|
|
129
|
+
async resolveUsers(_groupSlugs) {
|
|
130
|
+
return []
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
|
|
135
|
+
auditQueryPlugin(),
|
|
136
|
+
|
|
137
|
+
emailPlugin({
|
|
138
|
+
inngest,
|
|
139
|
+
// TODO: replace with your real user lookups
|
|
140
|
+
resolveApprover: async (_userId) => null,
|
|
141
|
+
resolveRequester: async (_userId) => null,
|
|
142
|
+
buildActionUrl: ({ token, action }) =>
|
|
143
|
+
`${process.env.NEXT_PUBLIC_SERVER_URL}/api/approvals/decision?token=${token}&action=${action}`,
|
|
144
|
+
tokens: { brandName: '{{projectName}}' },
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
formsPlugin({
|
|
148
|
+
inngest,
|
|
149
|
+
ipHashSecret: process.env.FORMS_IP_HASH_SECRET!,
|
|
150
|
+
// TODO: list every place a form is allowed to send to. The plugin
|
|
151
|
+
// refuses to deliver to destinations that aren't in this list.
|
|
152
|
+
allowedDestinations: [
|
|
153
|
+
// {
|
|
154
|
+
// type: 'email',
|
|
155
|
+
// value: 'team@example.com',
|
|
156
|
+
// label: 'Main inbox',
|
|
157
|
+
// description: 'General contact form destination',
|
|
158
|
+
// },
|
|
159
|
+
],
|
|
160
|
+
}),
|
|
161
|
+
|
|
162
|
+
integrationsPlugin({ inngest }),
|
|
163
|
+
],
|
|
164
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "preserve",
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"incremental": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"plugins": [{ "name": "next" }],
|
|
13
|
+
"paths": {
|
|
14
|
+
"@/*": ["./src/*"]
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"next-env.d.ts",
|
|
19
|
+
"**/*.ts",
|
|
20
|
+
"**/*.tsx",
|
|
21
|
+
".next/types/**/*.ts"
|
|
22
|
+
],
|
|
23
|
+
"exclude": ["node_modules", ".next"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "turbo run build",
|
|
7
|
+
"dev": "turbo run dev",
|
|
8
|
+
"lint": "turbo run lint",
|
|
9
|
+
"typecheck": "turbo run typecheck",
|
|
10
|
+
"test": "turbo run test",
|
|
11
|
+
"format": "prettier --write ."
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"prettier": "^3.4.0",
|
|
15
|
+
"turbo": "^2.3.0",
|
|
16
|
+
"typescript": "^5.6.0"
|
|
17
|
+
},
|
|
18
|
+
"packageManager": "pnpm@10.33.0",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20.9.0",
|
|
21
|
+
"pnpm": ">=10.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"exactOptionalPropertyTypes": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"resolveJsonModule": true
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://turborepo.com/schema.json",
|
|
3
|
+
"ui": "stream",
|
|
4
|
+
"tasks": {
|
|
5
|
+
"build": {
|
|
6
|
+
"dependsOn": ["^build"],
|
|
7
|
+
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
|
|
8
|
+
"env": ["NODE_ENV"]
|
|
9
|
+
},
|
|
10
|
+
"dev": {
|
|
11
|
+
"cache": false,
|
|
12
|
+
"persistent": true
|
|
13
|
+
},
|
|
14
|
+
"lint": {
|
|
15
|
+
"dependsOn": ["^build"]
|
|
16
|
+
},
|
|
17
|
+
"typecheck": {
|
|
18
|
+
"dependsOn": ["^build"]
|
|
19
|
+
},
|
|
20
|
+
"test": {
|
|
21
|
+
"dependsOn": ["^build"]
|
|
22
|
+
},
|
|
23
|
+
"clean": {
|
|
24
|
+
"cache": false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{#if packageScope}}@{{packageScope}}/design-system{{else}}{{projectName}}-design-system{{/if}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./manifest": "./src/manifest.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@forumone/throughline-reference-ds": "^0.1.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.6.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Re-export the Throughline reference design system. Wrap or override
|
|
2
|
+
// individual components here when you start building your own brand layer
|
|
3
|
+
// (e.g. swap colors, replace Hero, add new components). When you have
|
|
4
|
+
// enough custom work to stand alone, drop the reference DS dependency
|
|
5
|
+
// and replace this file with your own component exports.
|
|
6
|
+
export * from '@forumone/throughline-reference-ds'
|