|
| 1 | +--- |
| 2 | +name: setup-admin-ui |
| 3 | +description: Set up the OpenSaaS Stack Admin UI in an existing Next.js App Router project. Invoke as a forked subagent after migration is complete, passing the project root, desired admin path, and whether auth is enabled. |
| 4 | +context: fork |
| 5 | +agent: general-purpose |
| 6 | +--- |
| 7 | + |
| 8 | +Set up the OpenSaaS Stack Admin UI in the Next.js project described below. |
| 9 | + |
| 10 | +$ARGUMENTS |
| 11 | + |
| 12 | +## What This Skill Does |
| 13 | + |
| 14 | +1. Installs `@opensaas/stack-ui` if not already a dependency |
| 15 | +2. Creates the catch-all admin route page at `app/{adminPath}/[[...{segmentName}]]/page.tsx` |
| 16 | +3. Validates the generated file works with the project structure |
| 17 | + |
| 18 | +## Step 1 — Check if `@opensaas/stack-ui` Is Already Installed |
| 19 | + |
| 20 | +Read `package.json` in the project root. If `@opensaas/stack-ui` is **not** in `dependencies`, install it: |
| 21 | + |
| 22 | +```bash |
| 23 | +# Detect package manager |
| 24 | +# - If pnpm-lock.yaml exists → pnpm add @opensaas/stack-ui |
| 25 | +# - If yarn.lock exists → yarn add @opensaas/stack-ui |
| 26 | +# - Otherwise → npm install @opensaas/stack-ui |
| 27 | +``` |
| 28 | + |
| 29 | +Check existing versions of `@opensaas/stack-core` in `package.json` and install `@opensaas/stack-ui` at the **same version** to avoid mismatches. |
| 30 | + |
| 31 | +## Step 2 — Determine the Admin Path |
| 32 | + |
| 33 | +The admin path comes from `$ARGUMENTS` (e.g. `/admin`, `/dashboard/admin`). |
| 34 | + |
| 35 | +- **Route path**: `app/{adminPath}/[[...{segmentName}]]/page.tsx` |
| 36 | + - For `/admin` → `app/admin/[[...admin]]/page.tsx` |
| 37 | + - For `/dashboard/admin` → `app/dashboard/admin/[[...admin]]/page.tsx` |
| 38 | + - For `/cms` → `app/cms/[[...cms]]/page.tsx` |
| 39 | +- **Segment name** (used in both the directory name and the `params.{segmentName}` reference): the last path segment (e.g. `admin`, `cms`) |
| 40 | +- **`basePath`** prop on `<AdminUI>`: the full admin path (e.g. `/admin`, `/dashboard/admin`) |
| 41 | + |
| 42 | +Create all intermediate directories as needed. |
| 43 | + |
| 44 | +## Step 3 — Determine if Auth Is Enabled |
| 45 | + |
| 46 | +Check whether auth is configured: |
| 47 | + |
| 48 | +1. Look in `opensaas.config.ts` for `authPlugin` usage |
| 49 | +2. Check for a `lib/auth.ts` or `lib/auth/index.ts` that exports `getSession` |
| 50 | + |
| 51 | +This determines which page template to use. |
| 52 | + |
| 53 | +## Step 4 — Create the Admin Page |
| 54 | + |
| 55 | +### Template A — Without Auth |
| 56 | + |
| 57 | +Use this when auth is NOT configured: |
| 58 | + |
| 59 | +```typescript |
| 60 | +import { AdminUI } from '@opensaas/stack-ui' |
| 61 | +import type { ServerActionInput } from '@opensaas/stack-ui/server' |
| 62 | +import { getContext, config } from '@/.opensaas/context' |
| 63 | + |
| 64 | +async function serverAction(props: ServerActionInput) { |
| 65 | + 'use server' |
| 66 | + const context = await getContext() |
| 67 | + return await context.serverAction(props) |
| 68 | +} |
| 69 | + |
| 70 | +interface AdminPageProps { |
| 71 | + params: Promise<{ {SEGMENT_NAME}?: string[] }> |
| 72 | + searchParams: Promise<{ [key: string]: string | string[] | undefined }> |
| 73 | +} |
| 74 | + |
| 75 | +export default async function AdminPage({ params, searchParams }: AdminPageProps) { |
| 76 | + const resolvedParams = await params |
| 77 | + const resolvedSearchParams = await searchParams |
| 78 | + return ( |
| 79 | + <AdminUI |
| 80 | + context={await getContext()} |
| 81 | + config={await config} |
| 82 | + params={resolvedParams.{SEGMENT_NAME}} |
| 83 | + searchParams={resolvedSearchParams} |
| 84 | + basePath="{ADMIN_PATH}" |
| 85 | + serverAction={serverAction} |
| 86 | + /> |
| 87 | + ) |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +### Template B — With Auth |
| 92 | + |
| 93 | +Use this when `authPlugin` is detected and `getSession` is available: |
| 94 | + |
| 95 | +```typescript |
| 96 | +import { AdminUI } from '@opensaas/stack-ui' |
| 97 | +import type { ServerActionInput } from '@opensaas/stack-ui/server' |
| 98 | +import { getContext, config } from '@/.opensaas/context' |
| 99 | +import { getSession } from '@/lib/auth' |
| 100 | + |
| 101 | +async function serverAction(props: ServerActionInput) { |
| 102 | + 'use server' |
| 103 | + const context = await getContext({ session: await getSession() }) |
| 104 | + return await context.serverAction(props) |
| 105 | +} |
| 106 | + |
| 107 | +interface AdminPageProps { |
| 108 | + params: Promise<{ {SEGMENT_NAME}?: string[] }> |
| 109 | + searchParams: Promise<{ [key: string]: string | string[] | undefined }> |
| 110 | +} |
| 111 | + |
| 112 | +export default async function AdminPage({ params, searchParams }: AdminPageProps) { |
| 113 | + const resolvedParams = await params |
| 114 | + const resolvedSearchParams = await searchParams |
| 115 | + const session = await getSession() |
| 116 | + if (!session) { |
| 117 | + return ( |
| 118 | + <div className="p-8"> |
| 119 | + <div className="bg-destructive/10 border border-destructive text-destructive rounded-lg p-6"> |
| 120 | + <h2 className="text-lg font-semibold mb-2">Access Denied</h2> |
| 121 | + <p>You must be logged in to access the admin interface.</p> |
| 122 | + </div> |
| 123 | + </div> |
| 124 | + ) |
| 125 | + } |
| 126 | + return ( |
| 127 | + <AdminUI |
| 128 | + context={await getContext(session)} |
| 129 | + config={await config} |
| 130 | + params={resolvedParams.{SEGMENT_NAME}} |
| 131 | + searchParams={resolvedSearchParams} |
| 132 | + basePath="{ADMIN_PATH}" |
| 133 | + serverAction={serverAction} |
| 134 | + /> |
| 135 | + ) |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +Replace `{SEGMENT_NAME}` with the last segment of the admin path (e.g. `admin`) and `{ADMIN_PATH}` with the full path (e.g. `/admin`). |
| 140 | + |
| 141 | +**Important**: If `getSession` is not at `@/lib/auth`, check for it at its actual location (e.g. `@/lib/auth/index`, `@/app/lib/auth`) and use the correct import path. |
| 142 | + |
| 143 | +## Step 5 — Check for Missing `.opensaas/context` |
| 144 | + |
| 145 | +The admin page imports from `@/.opensaas/context`. This file is generated by `pnpm opensaas generate` (or `pnpm generate`). If it doesn't exist yet: |
| 146 | + |
| 147 | +- **Do not create it manually** — it's generated from `opensaas.config.ts` |
| 148 | +- Remind the user to run `pnpm generate` (or `pnpm opensaas generate`) before starting the dev server |
| 149 | + |
| 150 | +## Step 6 — Report What Was Done |
| 151 | + |
| 152 | +Report to the user: |
| 153 | + |
| 154 | +``` |
| 155 | +✓ Admin UI set up at: {adminPath} |
| 156 | +✓ File created: app/{adminPath}/[[...{segmentName}]]/page.tsx |
| 157 | +✓ Auth-aware: yes/no |
| 158 | +✓ @opensaas/stack-ui: already installed / installed at version X.Y.Z |
| 159 | +
|
| 160 | +Next steps: |
| 161 | +1. Run `pnpm generate` to generate the .opensaas/context.ts file (if not already done) |
| 162 | +2. Run `pnpm dev` to start the dev server |
| 163 | +3. Visit http://localhost:3000{adminPath} to access the admin UI |
| 164 | +
|
| 165 | +Docs: https://stack.opensaas.au/admin-ui |
| 166 | +``` |
| 167 | + |
| 168 | +## Notes |
| 169 | + |
| 170 | +- The `[[...{segmentName}]]` catch-all segment handles all admin routes: the list view, item detail, create, and edit pages — all from one file. |
| 171 | +- The `basePath` prop must match exactly the URL path where the admin is mounted. |
| 172 | +- The `serverAction` wrapper function is required — it provides the server action bridge between the client-side admin UI components and the database context. |
| 173 | +- If the user is using a custom `getSession` approach (not from `@opensaas/stack-auth`), the auth template still works — just update the import path and the session shape passed to `getContext`. |
| 174 | +- If Tailwind CSS is not configured in the project, mention that `@opensaas/stack-ui` uses Tailwind for styling and link to the docs for setup: https://stack.opensaas.au/admin-ui#tailwind |
0 commit comments