diff --git a/.github/workflows/deploy_documentation.yaml b/.github/workflows/deploy_documentation.yaml index 2b7ca1f38..d867b65ff 100644 --- a/.github/workflows/deploy_documentation.yaml +++ b/.github/workflows/deploy_documentation.yaml @@ -5,7 +5,10 @@ on: branches: - main paths: - - 'adminforth/documentation/**' + - 'documentation/**' + # AFCL previews of the docs are built from these + - 'adminforth/spa/src/afcl/**' + - 'adminforth/modules/styles.ts' - '.github/workflows/deploy_documentation.yaml' jobs: @@ -13,7 +16,7 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: adminforth + working-directory: documentation steps: - name: Checkout repository uses: actions/checkout@v4 @@ -26,18 +29,23 @@ jobs: with: node-version: '24' cache: 'pnpm' - cache-dependency-path: adminforth/pnpm-lock.yaml + cache-dependency-path: | + adminforth/pnpm-lock.yaml + documentation/pnpm-lock.yaml - name: Configure Git run: | git config --global user.name "${{ github.actor }}" git config --global user.email "${{ github.actor }}@users.noreply.github.com" + # adminforth itself is needed too: the API pages are generated from its types, AFCL previews from its SPA - name: Install dependencies run: | - pnpm i - cd documentation - npm ci + pnpm --dir ../adminforth install + pnpm install - name: Build and deploy docs - run: GIT_USER=${{ github.actor }} GIT_PASS=${{ secrets.DOCUSAURUS_DEPLOY_KEY }} pnpm rollout-doc + env: + GIT_USER: ${{ github.actor }} + GIT_PASS: ${{ secrets.DOCUSAURUS_DEPLOY_KEY }} + run: pnpm run build && pnpm run deploy diff --git a/AGENTS.md b/AGENTS.md index ed1a2d337..83abe1c1d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,14 @@ All packages and projects in this repo use `pnpm` and not `npm`. Howeverer internally (e.g. in `codeInjector`) adminforth still supports both `npm` and `pnpm` style install commands, so users of framework itself can use it with either package manager. But in all dev demo/live demo, plugins, adapters, and documentation, we use `pnpm` as the standard. +## SPA build folder + +Never patch anything in `spa_tmp` (e.g. `/tmp/adminforth//spa_tmp`): no file edits, no `pnpm install`, no own vite or other commands there. `codeInjector` generates it from `adminforth/spa` and the custom components of the app and its plugins, a manual install there drops the packages it adds for plugins. After a change in `spa/package.json` or `spa/tailwind.config.js` the running app breaks until a restart, tell the user to restart it. + +## Dev demo login + +To log in to the local dev-demo (`http://localhost:3123`) use `agent` as username and `agent` as password. The user is created on start and is exempt from 2FA. Do not use `adminforth`, it may have 2FA set up in the local database. + ## Package names rules @@ -18,6 +26,14 @@ Same for adapters, but with `/docs/tutorial/Adapters//` path. Page names in docusarus should be human readabale. We should not use `AuditLog` but instead we should have `Audit Log` via whitespace. +## Design system + +UI rules (tokens, `af-`/`afcl-` class contract, AFCL first, Lucide icons, moving off flowbite) live in [Design concepts](adminforth/design/system-concepts.md), read them before changing UI in core or plugins. States and variants of components are drawn in `adminforth/design/system.html`, the progress of core and plugins is in `adminforth/design/adoption.md`. + +## Commits + +Never add `Co-authored-by` trailers or any other mention of an AI assistant (Claude, Codex, Copilot, "Generated with ...") to commit messages and PR descriptions. + ## General engineering rules Write code as if the system contracts are already defined and trusted. diff --git a/README.md b/README.md index 84b4f72d2..1b244ecc0 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,181 @@ -# AdminForth - powerfull Agent-first Typescript admin panel framework +
+ +AdminForth +# AdminForth — agent-first business admin - +**The back office your coding agent builds and your agents run. Your data, your server, your rules. A change history that names the agent.** -[![Ask AI](http://tluma.ai/badge)](http://tluma.ai/ask-ai/devforth/adminforth) +[Website](https://adminforth.dev) · [Start with your agent](#start-with-your-agent) · [Documentation](https://adminforth.dev/docs/tutorial/gettingStarted/) · [For developers](#for-developers) -* [Try live demo](https://demo.adminforth.dev/) +
-* [YouTube video: build agentic admin panel in a minutes](https://www.youtube.com/watch?v=4tB8uzY__uk) +adminforth-agent-demo + +You already work with AI agents. AdminForth is the admin they run your business through: customers, orders, jobs, stock and invoices in your own database, built by your coding agent, used by you and your team through Claude Code, Codex or any agent that speaks MCP. The panel is one way in; your agent is another. Screens are there when you want to look. + + + + An agent connected to an example business app through MCP: asked which follow-ups are overdue, it lists two requests, then updates the next action of REQ-1042 on the owner's request. + + +

Maya’s agent works on her company’s app through MCP, with her permissions. Northstar Services and its data are made up.

+ +

+ npm downloads + MIT license + GitHub stars + Ask AI +

+ +## What it gives you + +1. **Your data stays yours, and stays patched.** Records live in your own database on your server. Agents get in through one locked door: sign-in, a separate revocable key per agent, two-factor and passkeys for people. The security parts come from AdminForth, one framework our team maintains; updating is a version bump your agent runs. +2. **Your team and their agents work inside your rules.** Each person, and each of their agents, sees and changes only what their role allows. The server checks every request. The audit log names the person and the agent for every change. +3. **Screens, optional.** Ask your agent for the day’s work. Open a screen or a dashboard when you want to see it all. + + + + The audit log of the example app: Maya through Codex updated REQ-1042, Tom created a follow-up in the browser, Priya through Claude Code changed a status. Each row shows the old and the new value. + + +

Who changed what, when, through which agent, and what it was before.

+ +> **8 of the 10 biggest months for software vulnerabilities ever recorded happened in 2026.** [One in five of all vulnerabilities ever registered](https://cvetodo.com/cve-statistics) were published this year, and the year isn’t over ([monthly data](https://craci.com/resources/CVE-publication-trends)). Exploitation in the wild [nearly doubled](https://cloud.google.com/blog/topics/threat-intelligence/vulnerability-discovery-and-exploitation-trends-in-the-ai-era). An app your agent generated last month, with hundreds of packages nobody updates, sits right in that path. AdminForth, one framework maintained by our team, doesn’t. + +## Start with your agent + +1. Install [Claude Code](https://code.claude.com/docs/en/desktop-quickstart) or [Codex](https://learn.chatgpt.com/docs/quickstart) and sign in. +2. Create a folder for the project, for example `my-business`, and open the agent in it. +3. Paste the build instructions below. Change the lines under `MY BUSINESS BRIEF` to your own process first. + +The agent builds the first part with sample data, runs the app, connects itself to it through MCP and shows you one operation end to end. From then on, you talk to your business. + +
+Build instructions for your coding agent + +```text +Help me build a small business application with AdminForth. Work with me in +plain language. I don't have a developer: tell me exactly what to do myself, +step by step. + +MY BUSINESS BRIEF +BUSINESS_PROCESS: Customer requests for my maintenance business. +CURRENT_PROBLEM: We send quotes and forget to follow up. +TEAM_ACCESS: Me and two managers. Each manager handles their own requests. +CURRENT_DATA: A spreadsheet. Start this example with synthetic data. + +WHAT I WANT +A web app on AdminForth that my agent and my team's agents work with through +MCP, with sign-in, permissions and an audit log. I will mostly talk to it +through my agent; screens are for checking. Keep the first version small and +ready for more parts later (orders, jobs, stock, invoices). + +START HERE +Read https://devforth.io/admin/api/af-start/business/ first, then +https://adminforth.dev/docs/tutorial/gettingStarted/ and +https://adminforth.dev/llms.txt. Use AdminForth as the framework: reuse +its screens, forms, filters and access rules, don't build a separate stack. +Use pnpm and Node.js 24. For a new local example use SQLite, the documented +create-app flow and Prisma migrations. Read the generated AGENTS.md, CLAUDE.md +and skills before changing anything. If something can't run here, tell me what +I must do instead of pretending it works. + +Show me a short plain-language outline (records, roles, the first operation) +and ask only about decisions that change the data or the access rules. + +BUILD +1. Records and relationships for my process, named so my team understands + them, with a small set of fictional sample records. +2. Access rules on the server: I manage everything; every other person works + only with what their role allows, including direct record requests, lists + and relationships. A hidden button is not an access rule. +3. The MCP plugin (https://adminforth.dev/docs/tutorial/Plugins/mcp/) so I can + connect my agent, and the Audit Log plugin + (https://adminforth.dev/docs/tutorial/Plugins/audit-log/) with its + executed-by column, so every change names the person and the agent. +4. No production data, emails or payments in this first version. + +SHOW ME +Run the app and give me the local address and sign-in. Connect my agent with +the command from the Agents settings page, then do one operation through the +agent: find a record that needs attention, add its next action, and show me the +audit log entry. Show one action my agent is allowed and one a teammate's agent +would be refused. Say which checks you actually performed. + +Write five prompts for my daily work with this app, in the words of my process. +Explain where the code and data are, how to reopen the app tomorrow, how to +update AdminForth and its plugins later, and what remains before my team uses +it: hosting, real data, backups, access review. +``` -* [Hello world in 5 minutes](https://adminforth.dev/docs/tutorial/gettingStarted) with AdminForth +
-* [Tutorial](https://adminforth.dev/docs/tutorial/Customization/branding/) +Prompts for the first day, with the example above: -
+- `What needs my attention today?` +- `Which follow-ups are overdue, and whose?` +- `Add a request from Harbor Café: office maintenance quote. Owner Maya, next action: send the quote by Friday.` +- `Which invoices are unpaid for more than 30 days?` -
- AdminForth Agent -
+ + + The Overview page of the example app: open requests, overdue follow-ups, jobs this week, unpaid invoices, today's jobs and items low in stock. + +

When you want to look: the overview of the whole business, built with the dashboard plugin.

-
+> [!NOTE] +> The framework is free and MIT-licensed. Coding-agent access, model usage, hosting and paid services you connect have their own costs. Check each change in the app before your team relies on it. -Why AdminForth: +⭐ **Star AdminForth** to find it again when your agents need an admin to work in. -* Init AdminForth project with `npx adminforth create-app` and pass your database URL, import the tables you wish to see in admin using `npx adminforth resource`, and get fully functional UI for your data (filter, create, edit, remove) -* Modern look and simple Tailwind-ish ability to adjust it -* Supports Postgres, MySQL, Mongo, SQLite, Clickhouse -* Define Vue components to change look of various parts of admin using `npx adminforth component` (edit data cells, edit fields, add something above the table, inject something to header or sidebar, add custom page with charts or custom components) -* Build-in Components library (AdminForth AFCL) with premade easy-to-use build-blocks which follow your theme -* Define express APIs and call them from your components and pages -* Use various modern back-office-must-have plugins like audit log, files/image upload, TOTP 2FA, I18N, Copilot-style AI writing and image generation and many more -* AdminForth is always free and open-source (no paid versions, no cloud subscriptions sh*t) +## For developers +AdminForth is a TypeScript and Vue 3 framework for back-office and business applications. Declare resources over your database and get list, show, create and edit screens with filters, validation and access control, plus the same resources as MCP tools for agents. Everything else is code you own: + +- **Data sources:** Postgres, MySQL, SQLite, MongoDB, ClickHouse. +- **Agents:** the [MCP server plugin](https://adminforth.dev/docs/tutorial/Plugins/mcp/) with OAuth 2.1 sign-in or per-user revocable secrets, every call under the user’s permissions, validation and hooks; the [in-app agent](https://adminforth.dev/docs/tutorial/Plugins/agent/); resources and endpoints you can hide from agents; [audit log](https://adminforth.dev/docs/tutorial/Plugins/audit-log/) with agent attribution. +- **Security plugins:** TOTP and passkeys, OAuth SSO, user sessions with revoke, login captcha, signed password-reset tokens. +- **Extend in code:** hooks, custom actions, Express endpoints, Vue components injected into any part of the page, custom pages and the AFCL component library, styled with Tailwind. +- **More plugins:** import and export (CSV, XLSX), uploads to S3, i18n, rich editor, background jobs, dashboards and [more](https://adminforth.dev/docs/tutorial/ListOfPlugins/). +- **Made for coding agents:** generated apps ship `AGENTS.md`, `CLAUDE.md` and skills; the docs publish [`llms.txt`](https://adminforth.dev/llms.txt). + +[Hello world in 5 minutes](https://adminforth.dev/docs/tutorial/gettingStarted/) · [Live demo](https://demo.adminforth.dev/) · [Tutorial](https://adminforth.dev/docs/tutorial/Customization/branding/) · [YouTube: an agentic admin panel in minutes](https://www.youtube.com/watch?v=4tB8uzY__uk) ## Project initialisation AdminForth supports Node.js 22.12+, 24, and 26. Node.js 20 and 21 are no longer supported. We recommend Node.js 24 LTS for production. -AdminForth supports two setup paths: - ### Path 1: Existing database -Use this path when you already have a database and your own schema or migrations. Provide your database URL with `--db`, or enter it when the CLI asks `Please specify the database URL to use`. +Provide your database URL with `--db`, or enter it when the CLI asks `Please specify the database URL to use`: ```bash -npx adminforth create-app --app-name myadmin --db "postgresql://user:password@localhost:5432/dbname" +pnpm dlx adminforth@latest create-app --app-name myadmin --db "postgresql://user:password@localhost:5432/dbname" cd myadmin ``` -When you provide your own database URL, AdminForth connects to your database but does not create Prisma schema or migrations for it. The generated project README includes the SQL or schema notes needed to add the required `adminuser` table with your own migration tool. +The CLI inspects the database. When it already has tables, AdminForth connects to it without generating Prisma migrations, and the generated project README includes the SQL for the required `adminuser` table, to add with your own migration tool. When the database is empty, you can let AdminForth manage its schema with Prisma migrations. -After project creation, generate AdminForth resource files from your existing tables: +Then generate AdminForth resource files from your existing tables: ```bash -npx adminforth resource +pnpm exec adminforth resource ``` ### Path 2: New database -Use this path when you want AdminForth to scaffold a standalone app with a new local SQLite database. Omit `--db`, or accept the default `sqlite://.db.sqlite` value in the interactive prompt: +Omit `--db`, or accept the default `sqlite://.db.sqlite` value in the interactive prompt, and choose Prisma migrations: ```bash -npx adminforth create-app --app-name myadmin +pnpm dlx adminforth@latest create-app --app-name myadmin cd myadmin pnpm makemigration --name init && pnpm migrate:local pnpm dev ``` -For the new database path, the CLI can scaffold Prisma files and migration scripts for the default `sqlite://.db.sqlite` database. Please follow [getting started](https://adminforth.dev/docs/tutorial/gettingStarted/) for the full guide. +Please follow [getting started](https://adminforth.dev/docs/tutorial/gettingStarted/) for the full guide. # For AdminForth developers @@ -156,3 +262,9 @@ This will always run latest version of adminforth package. ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=devforth/adminforth&type=date&legend=top-left)](https://www.star-history.com/#devforth/adminforth&type=date&legend=top-left) + +## Want to talk through your process? + +[Book a free 30-minute consultation](https://talk.devforth.io/book/30-minute-meeting) about whether an admin built on AdminForth is a good fit for your business and what a useful first version could look like. The consultation is free; implementation and hosting are separate. + +Powered by Devforth diff --git a/adapters/AGENTS.md b/adapters/AGENTS.md index 0ebb8b39f..83f88974f 100644 --- a/adapters/AGENTS.md +++ b/adapters/AGENTS.md @@ -53,7 +53,7 @@ the section's heading anchor. Do not guess it — derive it from the docs source To build (or validate) an adapter's `homepage`: 1. Find the adapter's category page under - `adminforth/documentation/docs/tutorial/06-Adapters/*.md`. The page slug is + `documentation/docs/tutorial/06-Adapters/*.md`. The page slug is the filename without its numeric prefix (e.g. `07-key-value-adapters.md` → `key-value-adapters`). 2. On that page, find the `##` heading for this specific adapter. diff --git a/adminforth/.npmignore b/adminforth/.npmignore index 246c207d0..f4f388c88 100644 --- a/adminforth/.npmignore +++ b/adminforth/.npmignore @@ -1,2 +1 @@ -documentation plugins \ No newline at end of file diff --git a/adminforth/auth.ts b/adminforth/auth.ts index d43f2e303..5b2b0a60c 100644 --- a/adminforth/auth.ts +++ b/adminforth/auth.ts @@ -4,7 +4,7 @@ import crypto from 'crypto'; import AdminForth from './index.js'; import { AdminUserAuthorizationResult, AdminUserAuthorizeFunction, AfterSessionCreatedFunction, HttpExtra, IAdminForthAuth, IAdminForthHttpResponse } from './types/Back.js'; import { AdminUser } from './types/Common.js'; -import { listify } from './modules/utils.js'; +import { listify, removedInV4 } from './modules/utils.js'; import { afLogger } from './modules/logger.js'; import { Address4, Address6 } from 'ip-address'; @@ -128,17 +128,13 @@ class AdminForthAuth implements IAdminForthAuth { } setCustomCookie({ response, payload }: { - response: any, payload: { name: string, value: string, expiry?: number | undefined, expirySeconds: number | undefined, httpOnly: boolean, sessionBased?: boolean | undefined } + response: any, payload: { name: string, value: string, expirySeconds?: number, httpOnly: boolean, sessionBased?: boolean } }) { - const {name, value, expiry, httpOnly, expirySeconds, sessionBased } = payload; - - let expiryMs = 24 * 60 * 60 * 1000; // default 1 day - if (expirySeconds !== undefined) { - expiryMs = expirySeconds * 1000; - } else if (expiry !== undefined) { - afLogger.warn(`setCustomCookie: expiry(in ms) is deprecated, use expirySeconds instead (seconds), traceback: ${new Error().stack}`); - expiryMs = expiry; + if ('expiry' in payload) { + throw new Error(removedInV4('setCustomCookie payload "expiry"', 'expirySeconds')); } + const { name, value, httpOnly, expirySeconds = 24 * 60 * 60, sessionBased } = payload; // default 1 day + const expiryMs = expirySeconds * 1000; const brandSlug = this.adminforth.config.customization.brandNameSlug; response.setHeader('Set-Cookie', `adminforth_${brandSlug}_${name}=${value}; Path=${this.adminforth.config.baseUrl || '/'};${ @@ -171,7 +167,6 @@ class AdminForthAuth implements IAdminForthAuth { for (const hook of listify(adminUserAuthorize)) { const resp = await hook({ adminUser, - response, adminforth: this.adminforth, extra, }); diff --git a/adminforth/commands/createApp/templates/.agents/skills/adminforth-custom-vue/SKILL.md.hbs b/adminforth/commands/createApp/templates/.agents/skills/adminforth-custom-vue/SKILL.md.hbs index b6fb9d9fd..19284751c 100644 --- a/adminforth/commands/createApp/templates/.agents/skills/adminforth-custom-vue/SKILL.md.hbs +++ b/adminforth/commands/createApp/templates/.agents/skills/adminforth-custom-vue/SKILL.md.hbs @@ -51,6 +51,10 @@ not wait for a follow-up prompt about styling or dark mode. 8. **Never build Tailwind class names dynamically.** `custom/` is copied into the SPA sources and scanned statically by Tailwind, so `` `text-${color}-600` `` produces no CSS. Write full class strings and pick between them. +9. **Layouts follow their container, not the screen.** The sidebar and the agent panel take part of the + screen, so a page or panel which splits into columns is a Tailwind size container: `@container` on its + root and `@md:`, `@4xl:` variants on the grid instead of `md:`, `lg:`. Give the grid `grid-cols-1` as its + base, otherwise a wide chart or table stretches the single column past the page. ## Dark Theme Self-Check @@ -121,9 +125,11 @@ custom/ ```vue diff --git a/adminforth/commands/createApp/templates/index.ts.hbs b/adminforth/commands/createApp/templates/index.ts.hbs index 0a0005e39..bb3191319 100644 --- a/adminforth/commands/createApp/templates/index.ts.hbs +++ b/adminforth/commands/createApp/templates/index.ts.hbs @@ -70,7 +70,7 @@ export const admin = new AdminForth({ { type: 'heading', label: 'SYSTEM' }, { label: 'Users', - icon: 'flowbite:user-solid', + icon: 'lucide:user', resourceId: 'adminuser' }, ], diff --git a/adminforth/commands/createResource/injectResourceIntoIndex.js b/adminforth/commands/createResource/injectResourceIntoIndex.js index 14b44a32a..46bf84ff6 100644 --- a/adminforth/commands/createResource/injectResourceIntoIndex.js +++ b/adminforth/commands/createResource/injectResourceIntoIndex.js @@ -18,7 +18,7 @@ export async function injectResourceIntoIndex({ table, resourceId, label, - icon = "flowbite:user-solid", + icon = "lucide:user", }) { let code = await fs.readFile(indexFilePath, "utf-8"); const ast = recast.parse(code, { parser }); diff --git a/adminforth/commands/createResource/main.js b/adminforth/commands/createResource/main.js index be6aa621d..41799e498 100644 --- a/adminforth/commands/createResource/main.js +++ b/adminforth/commands/createResource/main.js @@ -59,6 +59,6 @@ export default async function createResource(args) { table: resourceId, resourceId: resourceId, label: toTitleCase(table.table), - icon: "flowbite:user-solid", + icon: "lucide:user", }); } diff --git a/adminforth/dataConnectors/baseConnector.ts b/adminforth/dataConnectors/baseConnector.ts index 55fbf9f03..6c732d6e4 100644 --- a/adminforth/dataConnectors/baseConnector.ts +++ b/adminforth/dataConnectors/baseConnector.ts @@ -10,7 +10,7 @@ import type { AdminUser } from "../types/Common.js" import { suggestIfTypo } from "../modules/utils.js"; import { decodeRecordId, encodeRecordId, isCompositePrimaryKey, primaryKeyColumnNames, primaryKeyColumns } from "../modules/recordId.js"; -import { interpretResource } from "../modules/restApi.js"; +import { interpretResource } from "../modules/resourceAccess.js"; import { ActionCheckSource, AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections, AllowedActionsEnum } from "../types/Common.js"; import { randomUUID } from "crypto"; import dayjs from "dayjs"; @@ -25,6 +25,40 @@ type AdminForthFilterNormalizationResult = { normalizedFilters?: AdminForthFilterInput; }; +function polymorphicFilterableFields(resource: AdminForthResource): Set | null { + const resources = global.adminforth?.config?.resources; + if (!resources) { + return null; + } + const siblingResourceIds = new Set(); + let isTarget = false; + for (const res of resources) { + for (const col of res.dataSourceColumns) { + const polymorphicResources = col.foreignResource?.polymorphicResources; + if (polymorphicResources?.some((pr) => pr.resourceId === resource.resourceId)) { + isTarget = true; + for (const pr of polymorphicResources) { + if (pr.resourceId) { + siblingResourceIds.add(pr.resourceId); + } + } + } + } + } + if (!isTarget) { + return null; + } + const fields = new Set(); + for (const res of resources) { + if (res.resourceId === resource.resourceId || siblingResourceIds.has(res.resourceId)) { + for (const col of res.dataSourceColumns) { + fields.add(col.name); + } + } + } + return fields; +} + async function publishShowPageUpdate(resource: AdminForthResource, recordId: string, updates: Record) { await global.adminforth.websocket.publish(`/showPage/${resource.resourceId}/${String(recordId)}`, { @@ -59,14 +93,6 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon client: any; - /** - * @deprecated Since 1.2.9. Will be removed in 4.0.0. Use .client instead. - */ - get db() { - afLogger.warn('.db is deprecated, use .client instead'); - return this.client; - } - setupClient(url: string, options?: { recovery?: boolean }): Promise { throw new Error('Method not implemented.'); } @@ -279,23 +305,16 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon } const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), normalizedFilter.field); - - let isPolymorphicTarget = false; - if (global.adminforth?.config?.resources) { - isPolymorphicTarget = global.adminforth.config.resources.some(res => - res.dataSourceColumns.some(col => - col.foreignResource?.polymorphicResources?.some(pr => - pr.resourceId === resource.resourceId - ) - ) - ); - } - if (isPolymorphicTarget) { - afLogger.trace(`⚠️ Field '${normalizedFilter.field}' not found in polymorphic target resource '${resource.resourceId}', allowing query to proceed.`); + + const polymorphicFields = polymorphicFilterableFields(resource); + const rightFieldAllowed = !comparingWithRightField + || resource.dataSourceColumns.some((col) => col.name == normalizedFilter.rightField) + || !!polymorphicFields?.has(normalizedFilter.rightField as string); + if (polymorphicFields?.has(normalizedFilter.field) && rightFieldAllowed) { + afLogger.trace(`Field '${normalizedFilter.field}' not in resource '${resource.resourceId}' but valid on a polymorphic sibling, allowing query to proceed.`); return { ok: true, error: '', normalizedFilters: normalizedFilter }; - } else { - throw new Error(`Field '${normalizedFilter.field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`); } + throw new Error(`Field '${normalizedFilter.field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`); } // value normalization if (comparingWithRightField) { diff --git a/adminforth/design/adoption.md b/adminforth/design/adoption.md new file mode 100644 index 000000000..6bec402ea --- /dev/null +++ b/adminforth/design/adoption.md @@ -0,0 +1,65 @@ +# Design system adoption + +Which packages follow the [design concepts](./system-concepts.md): tokens instead of palette colors, AFCL components, Lucide icons, no flowbite. Put ✓ only when the whole package is done and checked in light and dark theme. + +| Package | Done | +|---|---| +| adminforth (core) | | +| @adminforth/agent | ✓ | +| @adminforth/audit-log | ✓ | +| @adminforth/auto-remove | no UI | +| @adminforth/background-jobs | ✓ | +| @adminforth/bulk-ai-flow | ✓ | +| @adminforth/clone-row | ✓ | +| @adminforth/crud-approve-plugin | ✓ | +| @adminforth/dashboard | ✓ | +| @adminforth/email-invite | ✓ | +| @adminforth/email-password-reset | ✓ | +| @adminforth/foreign-inline-list | ✓ | +| @adminforth/foreign-inline-show | ✓ | +| @adminforth/i18n | ✓ | +| @adminforth/i18n-static | ✓ | +| @adminforth/import-export | ✓ | +| @adminforth/json-form | ✓ | +| @adminforth/list-in-place-edit | ✓ | +| @adminforth/login-captcha | ✓ | +| @adminforth/many2many | ✓ | +| @adminforth/markdown | ✓ | +| @adminforth/mcp | ✓ | +| @adminforth/oauth | ✓ | +| @adminforth/open-signup | ✓ | +| @adminforth/quick-filters | ✓ | +| @adminforth/rich-editor | ✓ | +| @adminforth/text-complete | ✓ | +| @adminforth/two-factors-auth | ✓ | +| @adminforth/universal-search | ✓ | +| @adminforth/upload | ✓ | +| @adminforth/user-sessions | ✓ | +| @adminforth/user-soft-delete | ✓ | + +## Releasing an adapted plugin + +- Moving a plugin to the design system makes it depend on adminforth 4 (AFCL components and Lucide icons ship with it), so apps on adminforth 3 must not get it with an update: set `peerDependencies.adminforth` to `^4.0.0` and commit with a `BREAKING CHANGE:` footer, semantic-release then publishes a new major (it ignores `feat!:`). Bump `devDependencies.adminforth` together with the lockfile once 4.0.0 is on npm, earlier `pnpm install --frozen-lockfile` fails on CI. +- Roll the release out to the live demo right away: bump the plugin (and `adminforth`, when the plugin needs a newer core) in `live-demo/app/package.json` and push to `main`, the push rebuilds demo.adminforth.dev. +- Redraw the card of the plugin on the landing page, if it has one, in the design system. + +## Landing page previews + +The cards of the landing gallery (`documentation/src/pages/index.tsx`) show a static HTML page of `documentation/static/landing-previews` (`preview: ''`) drawn in the design system. + +- Draw the screen of the plugin's core feature, not the whole page: keep one key element (an open menu, a form field, a dialog) and drop the columns, fields and buttons which don't explain the feature. A card is about 440px wide, so a screen with every column of the real page is unreadable. +- Copy the closest page (a list, an edit form, a settings tab) and take the markup, texts, Lucide icons and tokens from the components of the plugin, the shared classes are in `previews.css`. +- The screen is drawn at a fixed size of 16:10 in `.screen`, 880×550 by default, a smaller one makes the text bigger. `previews.js` scales it to the card, follows the light/dark switch of the landing page and draws the sidebar (`