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 new file mode 100644 index 000000000..788df908a --- /dev/null +++ b/adminforth/commands/createApp/templates/.agents/skills/adminforth-custom-vue/SKILL.md.hbs @@ -0,0 +1,340 @@ +--- +name: adminforth-custom-vue +description: "Use when implementing AdminForth custom Vue UI: field components, page injections, login or global injections, meta-driven component declarations, and frontend packages inside custom/." +user-invocable: true +--- + +# AdminForth Custom Vue Workflow + +## When to Use + +- Editing files under `custom/`. +- Adding custom field renderers or editors via `column.components`. +- Adding resource page injections, login injections, or global layout injections. +- Passing `meta` into reusable Vue components. +- Installing frontend packages used only by custom AdminForth Vue code. + +## `custom/` Directory and `@@/` + +- `custom/` is the frontend workspace for AdminForth custom Vue code. +- By default, `@@/Something.vue` means “resolve this path from `customComponentsDir` on the backend”, and `customComponentsDir` defaults to `./custom`. +- Example: `@@/reports/OrdersHeader.vue` maps to `./custom/reports/OrdersHeader.vue`. +- The same `@@/` prefix also works for assets and helper files that are bundled into the SPA. + +## Frontend Packages in `custom/` + +- Install frontend-only dependencies inside `custom/`, not in the app root. + +```bash +cd custom +{{packageManager}} install apexcharts vue3-apexcharts +``` + +- If `custom/package.json` does not exist yet, initialize the folder first. + +```bash +cd custom +{{packageManager}} init +``` + +## Organizing Components + +- You can freely create subfolders and split components into smaller children. +- Example structure: + +```text +custom/ + reports/ + OrdersTopPanel.vue + parts/ + OrdersTotals.vue + OrdersFilters.vue +``` + +- Example parent component using subcomponents: + +```vue + + + +``` + +- In AdminForth config you still reference only the entry component: + +```ts +beforeBreadcrumbs: '@@/reports/OrdersTopPanel.vue' +``` + +## Short vs Full Component Declaration + +- Short declaration is just a file string: + +```ts +show: '@@/RoomsCell.vue' +``` + +- Full declaration lets you pass `meta`: + +```ts +show: { + file: '@@/RoomsCell.vue', + meta: { + filler: '🟨', + }, +} +``` + +- Use full declaration when the same component should behave differently in different places. +- `meta` is passed into the Vue component as a prop. +- Common `meta` use cases are reusable display options, plugin settings, `thinEnoughToShrinkTable`, `afOrder`, or custom page layout flags. + +## Field Component Spots + +- `components.show`: custom value renderer on show page. +- `components.showRow`: replaces the full show-table row. +- `components.create`: custom editor on create page. +- `components.edit`: custom editor on edit page. +- `components.list`: custom value renderer in list cells. +- `components.filter`: custom filter input. + +## Field Component Props and Emits + +- `show` and `list` components should expect `column`, `record`, `resource`, `adminUser`, and optional `meta`. +- In practice, custom `edit` and `create` components receive `column`, `value`, `record`, `resource`, `adminUser`, `readonly`, and optional `meta`. +- Custom `edit` and `create` components can emit: + - `update:value` to change the current field value. + - `update:recordFieldValue` to change another field in the same record. + - `update:inValidity` to report custom validation state. + - `update:emptiness` to report custom emptiness rules. +- If you update hidden technical fields through `update:recordFieldValue`, the target column may need `allowModifyWhenNotShowInCreate` or `allowModifyWhenNotShowInEdit`. + +## Custom Edit Component Example + +```vue + + + +``` + +## Custom List and Show Example with `meta` + +```ts +{ + name: 'number_of_rooms', + components: { + show: { + file: '@@/RoomsCell.vue', + meta: { filler: '🟨', suffix: 'rooms' }, + }, + list: { + file: '@@/RoomsCell.vue', + meta: { filler: '🟦', suffix: 'r' }, + }, + }, +} +``` + +```vue + + + +``` + +## Resource Page Injection Spots + +- `pageInjections.list.beforeBreadcrumbs` +- `pageInjections.list.afterBreadcrumbs` +- `pageInjections.list.beforeActionButtons` +- `pageInjections.list.bottom` +- `pageInjections.list.threeDotsDropdownItems` +- `pageInjections.list.customActionIcons` +- `pageInjections.list.customActionIconsThreeDotsMenuItems` +- `pageInjections.list.tableBodyStart` +- `pageInjections.list.tableRowReplace` +- `pageInjections.show.beforeBreadcrumbs` +- `pageInjections.show.afterBreadcrumbs` +- `pageInjections.show.bottom` +- `pageInjections.show.threeDotsDropdownItems` +- `pageInjections.edit.beforeBreadcrumbs` +- `pageInjections.edit.afterBreadcrumbs` +- `pageInjections.edit.bottom` +- `pageInjections.edit.threeDotsDropdownItems` +- `pageInjections.create.beforeBreadcrumbs` +- `pageInjections.create.afterBreadcrumbs` +- `pageInjections.create.bottom` +- `pageInjections.create.threeDotsDropdownItems` + +## Login and Global Injection Spots + +- `customization.loginPageInjections.panelHeader` +- `customization.loginPageInjections.underInputs` +- `customization.loginPageInjections.underLoginButton` +- `customization.globalInjections.userMenu` +- `customization.globalInjections.header` +- `customization.globalInjections.sidebar` +- `customization.globalInjections.sidebarTop` +- `customization.globalInjections.everyPageBottom` + +## Top Injection Example + +```ts +options: { + pageInjections: { + list: { + beforeBreadcrumbs: { + file: '@@/reports/OrdersTopPanel.vue', + meta: { + title: 'Orders overview', + }, + }, + }, + }, +} +``` + +## List Injection Shrink Behavior + +- On list pages, AdminForth tries to keep the table itself scrollable when there are no large top or bottom injections. +- That shrink behavior is affected by four list injection spots: `beforeBreadcrumbs`, `afterBreadcrumbs`, `beforeActionButtons`, and `bottom`. +- If none of those four spots are used, the table tries to shrink into the viewport and vertical scrolling happens inside the table area. +- If any of those spots are used, AdminForth assumes the page may now need extra vertical space, so the table stops shrinking and page scrolling moves to the document body instead. +- If your injected panel is actually compact, set `meta.thinEnoughToShrinkTable: true` on that injection. +- Important: every injection in those four spots must set `thinEnoughToShrinkTable: true`, otherwise the table will still stay in non-shrinking mode. + +```ts +options: { + pageInjections: { + list: { + bottom: { + file: '@@/reports/OrdersSummary.vue', + meta: { + thinEnoughToShrinkTable: true, + }, + }, + }, + }, +} +``` + +## Row-Level List Injection Notes + +- `tableBodyStart` renders extra `` rows near the start of the table body and receives `resource`, `adminUser`, and `meta`. +- `tableRowReplace` replaces the default `` for each record and receives `record`, `resource`, `adminUser`, and `meta`. +- `customActionIcons` and `customActionIconsThreeDotsMenuItems` are per-record spots and receive `record`, `resource`, `adminUser`, `meta`, and `updateRecords`. + +## Practical Rules + +- Prefer simple string declarations until you actually need `meta`. +- Reuse one component with multiple full declarations instead of cloning similar files. +- Keep page injections small unless the layout intentionally becomes page-scrolling. +- Keep custom edit and create components explicit about validity and emptiness if the default input heuristics are not enough. \ No newline at end of file diff --git a/adminforth/commands/createApp/templates/.agents/skills/adminforth-hooks/SKILL.md.hbs b/adminforth/commands/createApp/templates/.agents/skills/adminforth-hooks/SKILL.md.hbs new file mode 100644 index 000000000..b8c42958f --- /dev/null +++ b/adminforth/commands/createApp/templates/.agents/skills/adminforth-hooks/SKILL.md.hbs @@ -0,0 +1,70 @@ +--- +name: adminforth-hooks +description: "Use when implementing or changing AdminForth hooks: beforeDatasourceRequest, afterDatasourceResponse, beforeSave, afterSave, and source-aware show or edit flows." +user-invocable: true +--- + +# AdminForth Hooks Workflow + +## When to Use + +- Editing `resource.hooks` in `resources/*.ts`. +- Deciding which hook stage should own filtering, normalization, enrichment, validation, or side effects. +- Handling the difference between show-page requests and edit-page initial load. + +## Hook Rules + +- Hooks execute on the backend during AdminForth request handling. +- Keep hooks fast because every awaited operation delays the current request. +- Return `{ ok: true }` to continue or `{ ok: false, error: 'message' }` to stop the flow. +- A hook can be a single async function or an array of async functions. + +## Available Hooks + +- `show.beforeDatasourceRequest` +- `show.afterDatasourceResponse` +- `list.beforeDatasourceRequest` +- `list.afterDatasourceResponse` +- `create.beforeSave` +- `create.afterSave` +- `edit.beforeSave` +- `edit.afterSave` +- `delete.beforeSave` +- `delete.afterSave` + +## Stage Selection + +- Use `beforeDatasourceRequest` for read-time scoping, filter changes, or blocking access before the datasource runs. +- Use `afterDatasourceResponse` for reshaping or enriching records after the datasource returns them. +- Use `beforeSave` for checks or data mutations that must happen before create, edit, or delete reaches the database. +- Use `afterSave` only for post-write side effects such as logging, notifications, or sync work. Errors there do not roll back data that already reached the database. + +## Show and Edit Behavior + +- `show` hooks are also used when AdminForth loads initial data for the edit page. +- Inside `show` hooks, inspect `query.source` on the hook params, not `extra.query`. +- `query.source === 'show'` means the real show page. +- `query.source === 'edit'` means the edit form initial-load flow. + +## Example + +```ts +hooks: { + show: { + afterDatasourceResponse: async ({ query, response }) => { + if (query.source === 'edit') { + // runs only when AdminForth loads initial values for the edit page + } else if (query.source === 'show') { + // runs only on the real show page + } + + return { ok: true }; + }, + }, +} +``` + +## Choosing Hooks vs Permissions + +- Prefer `adminforth-permissions` for straightforward role-based access control. +- Use hooks when the rule depends on specific record values, incoming filters, mutations, or request lifecycle timing. \ No newline at end of file diff --git a/adminforth/commands/createApp/templates/.agents/skills/adminforth-permissions/SKILL.md.hbs b/adminforth/commands/createApp/templates/.agents/skills/adminforth-permissions/SKILL.md.hbs new file mode 100644 index 000000000..ef9f663fb --- /dev/null +++ b/adminforth/commands/createApp/templates/.agents/skills/adminforth-permissions/SKILL.md.hbs @@ -0,0 +1,59 @@ +--- +name: adminforth-permissions +description: "Use when implementing or changing AdminForth access control: allowedActions, showIn callbacks, menu visibility, per-record restrictions, and permission-aware resource behavior." +user-invocable: true +--- + +# AdminForth Permissions Workflow + +## When to Use + +- Editing `options.allowedActions`, column `showIn.*` callbacks, menu visibility, or other permission-sensitive resource behavior. +- Deciding whether a rule belongs in `allowedActions`, `showIn[x]`, or a hook that blocks access for specific records. +- Explaining why hiding a menu item is not the same as real authorization. + +## Permission Model + +- `allowedActions` is the real per-resource permission layer for `list`, `show`, `create`, `edit`, `delete`, and `filter`. +- `showIn[x]` is the real per-column and per-page-flow control for whether a field is accessible in a given UI flow. +- Menu visibility is UX only. Hiding a resource from the menu does not block direct URL access or API access. +- For per-record restrictions, use hooks that run before datasource access or before save and return `{ ok: false, error: 'message' }`. + +## Allowed Actions Rules + +- `allowedActions` can be static booleans or async functions. +- The callback receives `adminUser`, `resource`, `meta`, `source`, and `adminforth`. +- `source` tells you why the permission is being checked. Available values are `displayButtons`, `listRequest`, `showRequest`, `editLoadRequest`, `editRequest`, `createRequest`, `deleteRequest`, `bulkActionRequest`, and `customActionRequest`. +- `allowedActions` is evaluated for both UI rendering and real requests, so keep it fast and avoid unnecessary database work. +- Use `source === ActionCheckSource.DisplayButtons` only when you intentionally want different UI visibility from actual request authorization. + +## Example + +```ts +import { ActionCheckSource } from 'adminforth'; + +{ + resourceId: 'orders', + options: { + allowedActions: { + list: ({ adminUser }) => adminUser.dbUser.role !== 'suspended', + delete: ({ adminUser, source }) => { + // `source` tells you why AdminForth is checking permission. + // This keeps the delete button visible, but blocks the real delete + // unless the request is made by a superadmin. + if (source === ActionCheckSource.DisplayButtons) { + return true; + } + + return adminUser.dbUser.role === 'superadmin'; + }, + }, + }, +} +``` + +## Choosing the Right Layer + +- Use `allowedActions` for coarse resource-level permissions such as role-based create, edit, delete, or list access. +- Use `showIn[x]` when a field should be visible or editable only in specific flows or for specific admins. +- Use hooks when access depends on actual record values, request filters, or lifecycle context. \ No newline at end of file diff --git a/adminforth/commands/createApp/templates/.agents/skills/adminforth/SKILL.md.hbs b/adminforth/commands/createApp/templates/.agents/skills/adminforth/SKILL.md.hbs new file mode 100644 index 000000000..89c5e0c86 --- /dev/null +++ b/adminforth/commands/createApp/templates/.agents/skills/adminforth/SKILL.md.hbs @@ -0,0 +1,42 @@ +--- +name: adminforth +description: "Use for general AdminForth app work: resources, index.ts, api.ts, schema.prisma, custom UI, Data API, adapters, plugins, and overall project structure. Use specialized skills for permissions, hooks, and custom Vue work." +user-invocable: true +--- + +# AdminForth App Workflow + +## When to Use + +- Editing `index.ts`, `api.ts`, `resources/*.ts`, `custom/**`, environment files, or deployment files. +- Adding a resource, menu item, custom page, component, custom API, plugin, or adapter. +- Deciding whether logic belongs in resource config, the Data API, a custom Express route, or frontend customization. +- Use `adminforth-permissions` when the task is mainly about access control. +- Use `adminforth-hooks` when the task is mainly about lifecycle hooks. +- Use `adminforth-custom-vue` when the task is mainly about components under `custom/`, field renderers, or page/login/global injections. + +## Project Map + +- `index.ts`: main AdminForth config, resource registration, menu, and server startup. +- `api.ts`: custom Express endpoints. Prefer schema-aware handlers so request and response shapes stay explicit. +- `resources/*.ts`: table or collection definitions, columns, labels, filters, and resource-level behavior. +- `custom/`: Vue components, custom pages, injections, and static assets. +{{#if prismaDbUrl}}- `schema.prisma`: optional schema and migration source when this app uses Prisma.{{/if}} +- `.env.local` and `.env`: local config and secrets. + +## Recommended Workflow + +1. Change the database schema with Prisma or your own migration tool. AdminForth itself does not change the database schema for you. +2. Add or update the matching resource in `resources/*.ts`. +3. Register the resource and menu entry in `index.ts`. +4. Prefer resource config for labels, field visibility, defaults, and validation before reaching for custom code. +5. Use AdminForth Data API for simple CRUD, filtering, sorting, and counts. Use your own ORM or query builder for joins, aggregations, or more complex queries. +6. Put custom business endpoints in `api.ts`. +7. Put custom UI in `custom/`. + +## Commands + +- Install: `{{packageManager}} install` +- Start local development: `{{packageManagerRun}} dev` +{{#if prismaDbUrl}}- Apply local migrations: `{{packageManagerRun}} migrate:local` +- Create a migration: `{{packageManagerRun}} makemigration{{packageManagerScriptArgSeparator}}--name `{{/if}} \ No newline at end of file diff --git a/adminforth/commands/createApp/templates/AGENTS.md.hbs b/adminforth/commands/createApp/templates/AGENTS.md.hbs new file mode 100644 index 000000000..9bf656c1a --- /dev/null +++ b/adminforth/commands/createApp/templates/AGENTS.md.hbs @@ -0,0 +1,71 @@ +# AdminForth App Guide + +## Package manager + +This scaffold was generated with `{{packageManager}}`. Use `{{packageManager}}` commands in this project unless you intentionally migrate the tooling. + +## Project map + +- `index.ts` is the main AdminForth configuration and server entrypoint. +- `api.ts` is for custom Express APIs and request/response schemas. +- `resources/*.ts` defines resources, columns, labels, permissions, hooks, and display behavior. +- `custom/` is for custom Vue components, pages, injections, and static assets. +{{#if prismaDbUrl}}- `schema.prisma` is the schema and migration source if you use Prisma for this app.{{/if}} +- `.env.local` stores local non-sensitive defaults. `.env` stores secrets. In production prefer real environment variables or a secret manager. + +## Docs sources for agents + +- Use `https://adminforth.dev/llms.txt` for quick navigation across the AdminForth docs corpus. +- Use `https://adminforth.dev/llms-full.txt` (`llms full`) for the full single-file AdminForth reference. +- Prefer these sources before broad web search when the task is mainly about AdminForth APIs, plugins, adapters, or customization. + +## Common commands + +- Install dependencies: `{{packageManager}} install` +- Start the app locally: `{{packageManagerRun}} dev` +{{#if prismaDbUrl}}- Apply local migrations: `{{packageManagerRun}} migrate:local` +- Create a new migration and apply it locally: `{{packageManagerRun}} makemigration{{packageManagerScriptArgSeparator}}--name `{{/if}} + +## AdminForth workflow + +- AdminForth connects to existing database and gives a back-office over the data (CRUD, filtering, sorting, etc). +- One table or collection maps to one resource. +- Resource config database type agnostic (same for all db types). +- Add new resources in `resources/`, register them in `index.ts`, and add menu entries if you want them visible in the sidebar. +- Resource config should mandatory list all column names which AdminFramework should be aware of, column name should match the name in the database table/collection. +- For relational schema-based dbs types of columns in resource are optional and used for overriding where possible, however AdminForth by default fetches physycal types and manual specification is not required. +- For NoSQL or schemaless dbs types of columns in resource are required for AdminForth to work. +- AdminForth does not manage database schema. By default project ships Prisma but developer can use own migration tool, and update only through `resources/*.ts` and `index.ts`. +- Always create `recordLabel` to change on UI how one record is represented across the app. Try to include the most identifying information but human-readable in the label but keep it short for readability (avoid IDs if possible). +- Use `fillOnCreate` to fill with default values on record creation on backend. Avoid showIn.create=true + fillOnCreate for the same field. +- Use `suggestOnCreate` to prefill input with a suggested value on the frontend. +- Use `enum`, `minLength`, `maxLength` in column config to enforce validation rules. + +- Use AdminForth Data API for simple CRUD, filtering, sorting, and counts. For joins, aggregations, or heavier queries, use custom ORM or query builder. +- Use `api.ts` for custom business endpoints that do not fit standard resource CRUD. +- Use `custom/` for custom pages, field rendering, injections, and other Vue-side customization. + +### Access control + +- Keep access control explicit and backend-enforced. Menu visibility is only UX and is not a permission boundary. +- For permission-specific implementation details, examples, and routing between `allowedActions`, `showIn[x]`, and hooks, use the `adminforth-permissions` skill. + +### Hooks + +- Hooks run on the backend as part of AdminForth request flows, so keep them focused and fast. +- For hook stage selection, show-vs-edit behavior, and implementation examples, use the `adminforth-hooks` skill. + +## Engineering conventions + +- Trust typed internal contracts. Do not add duplicate validation for values already guaranteed by schema, types, or backend responses. +- Validate once at the boundary, not repeatedly in downstream consumers. +- Prefer small explicit helpers and named constants over speculative fallback logic. +- Extract reusable or non-trivial regexes into named constants instead of scattering inline regexes through business logic. +- Keep changes minimal, DRY, and YAGNI. + +## Before you change the app + +- If you add a new resource, register it in `index.ts` and add it to the menu if needed. +{{#if prismaDbUrl}}- If you change `schema.prisma`, create and apply a migration locally before testing the UI.{{/if}} +- If you add custom frontend code under `custom/`, install any extra frontend packages in `custom/package.json`. +- Default local login is `adminforth` / `adminforth` unless you changed the seeded admin user. \ No newline at end of file diff --git a/adminforth/commands/createApp/templates/CLAUDE.md.hbs b/adminforth/commands/createApp/templates/CLAUDE.md.hbs new file mode 100644 index 000000000..f093aa041 --- /dev/null +++ b/adminforth/commands/createApp/templates/CLAUDE.md.hbs @@ -0,0 +1,5 @@ +@AGENTS.md + +# Claude-specific notes + +When working with this project, follow the AdminForth conventions from AGENTS.md. \ No newline at end of file diff --git a/adminforth/commands/createApp/templates/Dockerfile.hbs b/adminforth/commands/createApp/templates/Dockerfile.hbs index f1cb87be6..d5c975d4d 100644 --- a/adminforth/commands/createApp/templates/Dockerfile.hbs +++ b/adminforth/commands/createApp/templates/Dockerfile.hbs @@ -1,19 +1,8 @@ -{{#unless useNpm}} -FROM devforth/node20-pnpm:latest +FROM {{dockerBaseImage}} WORKDIR /code/ -ADD package.json pnpm-lock.yaml pnpm-workspace.yaml /code/ -RUN pnpm i +ADD package.json {{dockerAdditionalManifestFiles}} /code/ +RUN {{packageManager}} {{dockerPackageInstallSubcommand}} ADD . /code/ -RUN pnpm exec adminforth bundle -CMD ["sh", "-c", "pnpm migrate:prod && pnpm prod"] -{{/unless}} -{{#if useNpm}} -FROM node:{{nodeMajor}}-slim -WORKDIR /code/ -ADD package.json package-lock.json /code/ -RUN npm ci -ADD . /code/ -RUN npx adminforth bundle -CMD ["sh", "-c", "npm run migrate:prod && npm run prod"] -{{/if}} \ No newline at end of file +RUN {{packageManagerExec}} adminforth bundle +CMD ["sh", "-c", "{{packageManagerRun}} migrate:prod && {{packageManagerRun}} prod"] \ No newline at end of file diff --git a/adminforth/commands/createApp/templates/package.json.hbs b/adminforth/commands/createApp/templates/package.json.hbs index 6b972ea13..8ca94bd4e 100644 --- a/adminforth/commands/createApp/templates/package.json.hbs +++ b/adminforth/commands/createApp/templates/package.json.hbs @@ -8,26 +8,14 @@ "license": "ISC", "description": "", "scripts": { - {{#unless useNpm}} - "dev": "pnpm _env:dev tsx watch index.ts", - "prod": "pnpm _env:prod tsx index.ts", - "start": "pnpm dev", - "makemigration": "pnpm _env:dev npx --yes prisma migrate dev --create-only", - "migrate:local": "pnpm _env:dev npx --yes prisma migrate deploy", - "migrate:prod": "pnpm _env:prod npx --yes prisma migrate deploy", + "dev": "{{packageManagerEnvDev}} tsx watch index.ts", + "prod": "{{packageManagerEnvProd}} tsx index.ts", + "start": "{{packageManagerRun}} dev", + "makemigration": "{{packageManagerEnvDev}} npx --yes prisma migrate dev --create-only", + "migrate:local": "{{packageManagerEnvDev}} npx --yes prisma migrate deploy", + "migrate:prod": "{{packageManagerEnvProd}} npx --yes prisma migrate deploy", "_env:dev": "dotenvx run -f .env -f .env.local --", "_env:prod": "dotenvx run -f .env.prod --" - {{/unless}} - {{#if useNpm}} - "dev": "npm run _env:dev -- tsx watch index.ts", - "prod": "npm run _env:prod -- tsx index.ts", - "start": "npm run dev", - "makemigration": "npm run _env:dev -- npx --yes prisma migrate dev --create-only", - "migrate:local": "npm run _env:dev -- npx --yes prisma migrate deploy", - "migrate:prod": "npm run _env:prod -- npx --yes prisma migrate deploy", - "_env:dev": "dotenvx run -f .env -f .env.local --", - "_env:prod": "dotenvx run -f .env.prod --" - {{/if}} }, "engines": { "node": ">=20" diff --git a/adminforth/commands/createApp/templates/readme.md.hbs b/adminforth/commands/createApp/templates/readme.md.hbs index 3dfbf858c..355413e45 100644 --- a/adminforth/commands/createApp/templates/readme.md.hbs +++ b/adminforth/commands/createApp/templates/readme.md.hbs @@ -1,41 +1,22 @@ ## Starting the application Install dependencies: -{{#if useNpm}} -```bash -npm i -``` - -Migrate the database: - -```bash -npm run migrate:local -``` - -Start the server: - -```bash -npm run dev -``` -{{/if}} -{{#unless useNpm}} ```bash -pnpm i +{{packageManager}} install ``` Migrate the database: ```bash -pnpm migrate:local +{{packageManagerRun}} migrate:local ``` Start the server: ```bash -pnpm dev +{{packageManagerRun}} dev ``` -{{/unless}} {{#if prismaDbUrl}} ## Changing schema @@ -45,10 +26,10 @@ Open `schema.prisma` and change schema as needed: add new tables, columns, etc ( Run the following command to generate a new migration and apply it instantly in local database: ```bash -pnpm makemigration --name +{{packageManagerRun}} makemigration{{packageManagerScriptArgSeparator}}--name ``` -Your colleagues will need to pull the changes and run `pnpm migrate:local` to apply the migration in their local database. +Your colleagues will need to pull the changes and run `{{packageManagerRun}} migrate:local` to apply the migration in their local database. {{/if}} ## Deployment tips diff --git a/adminforth/commands/createApp/utils.js b/adminforth/commands/createApp/utils.js index 072db41a7..f5cb0db98 100644 --- a/adminforth/commands/createApp/utils.js +++ b/adminforth/commands/createApp/utils.js @@ -260,11 +260,26 @@ async function scaffoldProject(ctx, options, cwd) { return projectDir; // Return the new directory path } +function getPackageManagerTemplateData(useNpm, nodeMajor) { + return { + packageManager: useNpm ? 'npm' : 'pnpm', + packageManagerRun: useNpm ? 'npm run' : 'pnpm', + packageManagerScriptArgSeparator: useNpm ? ' -- ' : ' ', + packageManagerExec: useNpm ? 'npx' : 'pnpm exec', + packageManagerEnvDev: useNpm ? 'npm run _env:dev --' : 'pnpm _env:dev', + packageManagerEnvProd: useNpm ? 'npm run _env:prod --' : 'pnpm _env:prod', + dockerBaseImage: useNpm ? `node:${nodeMajor}-slim` : 'devforth/node20-pnpm:latest', + dockerAdditionalManifestFiles: useNpm ? 'package-lock.json' : 'pnpm-lock.yaml pnpm-workspace.yaml', + dockerPackageInstallSubcommand: useNpm ? 'ci' : 'i', + }; +} + async function writeTemplateFiles(dirname, cwd, useNpm, options) { const { dbUrl, prismaDbUrl, appName, provider, nodeMajor, dbUrlProd, prismaDbUrlProd, sqliteFile } = options; + const packageManagerTemplateData = getPackageManagerTemplateData(useNpm, nodeMajor); // Build a list of files to generate const templateTasks = [ @@ -312,7 +327,37 @@ async function writeTemplateFiles(dirname, cwd, useNpm, options) { { src: 'readme.md.hbs', dest: 'README.md', - data: { dbUrl, prismaDbUrl, appName, sqliteFile, useNpm }, + data: { dbUrl, prismaDbUrl, appName, sqliteFile }, + }, + { + src: 'AGENTS.md.hbs', + dest: 'AGENTS.md', + data: { prismaDbUrl }, + }, + { + src: 'CLAUDE.md.hbs', + dest: 'CLAUDE.md', + data: {}, + }, + { + src: '.agents/skills/adminforth/SKILL.md.hbs', + dest: '.agents/skills/adminforth/SKILL.md', + data: { prismaDbUrl }, + }, + { + src: '.agents/skills/adminforth-permissions/SKILL.md.hbs', + dest: '.agents/skills/adminforth-permissions/SKILL.md', + data: {}, + }, + { + src: '.agents/skills/adminforth-hooks/SKILL.md.hbs', + dest: '.agents/skills/adminforth-hooks/SKILL.md', + data: {}, + }, + { + src: '.agents/skills/adminforth-custom-vue/SKILL.md.hbs', + dest: '.agents/skills/adminforth-custom-vue/SKILL.md', + data: {}, }, { // We'll write .env using the same content as .env.sample @@ -340,7 +385,7 @@ async function writeTemplateFiles(dirname, cwd, useNpm, options) { { src: 'Dockerfile.hbs', dest: 'Dockerfile', - data: { nodeMajor, useNpm }, + data: {}, }, { src: 'package.json.hbs', @@ -348,7 +393,6 @@ async function writeTemplateFiles(dirname, cwd, useNpm, options) { data: { appName, adminforthVersion: adminforthVersion, - useNpm }, }, { @@ -378,13 +422,16 @@ async function writeTemplateFiles(dirname, cwd, useNpm, options) { if (task.condition === false) continue; const destPath = path.join(cwd, task.dest); - // fse.ensureDirSync(path.dirname(destPath)); + await fse.ensureDir(path.dirname(destPath)); if (task.empty) { await fs.promises.writeFile(destPath, ''); } else { const templatePath = path.join(dirname, 'templates', task.src); - const compiled = renderHBSTemplate(templatePath, task.data); + const compiled = renderHBSTemplate(templatePath, { + ...packageManagerTemplateData, + ...task.data, + }); await fs.promises.writeFile(destPath, compiled); } } diff --git a/adminforth/documentation/docs/tutorial/001-gettingStarted.md b/adminforth/documentation/docs/tutorial/001-gettingStarted.md index a49e49dfa..d927d20ce 100644 --- a/adminforth/documentation/docs/tutorial/001-gettingStarted.md +++ b/adminforth/documentation/docs/tutorial/001-gettingStarted.md @@ -1,3 +1,7 @@ +--- +description: "Step-by-step guide to creating an AdminForth app with the CLI (bootstrapping), understanding the generated project structure, running migrations, and starting the server." +--- + # Getting Started This page provides a step-by-step guide to quickly get started with AdminForth using the `adminforth` CLI. diff --git a/adminforth/documentation/docs/tutorial/01-helloWorld.md b/adminforth/documentation/docs/tutorial/01-helloWorld.md index d739b0450..d118b9839 100644 --- a/adminforth/documentation/docs/tutorial/01-helloWorld.md +++ b/adminforth/documentation/docs/tutorial/01-helloWorld.md @@ -1,6 +1,7 @@ --- id: hello-world title: Hello world app without CLI +description: "Manual code way walkthrough (alternative to CLI) for building a minimal AdminForth app without the CLI, including project setup, resources, authentication, and server configuration." sidebar_class_name: hidden-sidebar --- diff --git a/adminforth/documentation/docs/tutorial/02-glossary.md b/adminforth/documentation/docs/tutorial/02-glossary.md index 17b15d2ca..53f3b5a1d 100644 --- a/adminforth/documentation/docs/tutorial/02-glossary.md +++ b/adminforth/documentation/docs/tutorial/02-glossary.md @@ -1,3 +1,7 @@ +--- +description: "Reference page defining core AdminForth terms such as data sources, resources, columns, records, actions, views, and other concepts used throughout the docs." +--- + # Glossary ## dataSource diff --git a/adminforth/documentation/docs/tutorial/03-Customization/01-branding.md b/adminforth/documentation/docs/tutorial/03-Customization/01-branding.md index a1a3f3c0e..4c14c4a7c 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/01-branding.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/01-branding.md @@ -1,5 +1,5 @@ --- -description: "Learn how to customize the branding and theming of your AdminForth application." +description: "Guide to customizing AdminForth branding and theming, including logos, titles, sidebar presentation, colors, fonts, and single-theme setups." image: "/ogs/branding.png" # Path to the OG image --- diff --git a/adminforth/documentation/docs/tutorial/03-Customization/02-customFieldRendering.md b/adminforth/documentation/docs/tutorial/03-Customization/02-customFieldRendering.md index f41fb4173..60f221695 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/02-customFieldRendering.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/02-customFieldRendering.md @@ -1,3 +1,7 @@ +--- +description: "Guide to replacing default field rendering with custom Vue components for list, show, create, edit, and filter views, including props and third-party packages." +--- + # Custom record field rendering ## Customizing how AdminForth renders the cells with record values diff --git a/adminforth/documentation/docs/tutorial/03-Customization/03-virtualColumns.md b/adminforth/documentation/docs/tutorial/03-Customization/03-virtualColumns.md index 720fc1462..43dff8f61 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/03-virtualColumns.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/03-virtualColumns.md @@ -1,3 +1,6 @@ +--- +description: "Guide to defining virtual columns for list, show, filter, edit, and create flows, including raw SQL and raw NoSQL query examples." +--- # Virtual columns diff --git a/adminforth/documentation/docs/tutorial/03-Customization/04-hooks.md b/adminforth/documentation/docs/tutorial/03-Customization/04-hooks.md index 84eeac3fd..1670a7879 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/04-hooks.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/04-hooks.md @@ -1,5 +1,5 @@ --- -description: "Hooks are powerful tools to modify the data before it is saved to the database, execute something after data were saved or deleted, change the query before fetching items from the database, modify the fetched data before it is displayed in the list and show, and prevent the request to db depending on some condition." +description: "Guide to AdminForth lifecycle hooks for list, show, create, edit, and delete flows, including request interception, response shaping, and save-time side effects." image: "/ogs/hooks.png" # Path to the OG image --- diff --git a/adminforth/documentation/docs/tutorial/03-Customization/05-limitingAccess.md b/adminforth/documentation/docs/tutorial/03-Customization/05-limitingAccess.md index 9d17189e7..481cf06ad 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/05-limitingAccess.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/05-limitingAccess.md @@ -1,3 +1,6 @@ +--- +description: "Guide to AdminForth access control with allowed actions, role-based rules, field visibility, and resource-specific restrictions based on users or record values." +--- # Limiting actions access diff --git a/adminforth/documentation/docs/tutorial/03-Customization/06-customPages.md b/adminforth/documentation/docs/tutorial/03-Customization/06-customPages.md index 1ba80a8db..45fe1387a 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/06-customPages.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/06-customPages.md @@ -1,5 +1,5 @@ --- -description: "Learn how to create custom pages in AdminForth." +description: "Guide to adding custom AdminForth pages, custom APIs, public or protected routes, menu entries, and page metadata for custom Vue pages." image: "/ogs/customPages.png" --- diff --git a/adminforth/documentation/docs/tutorial/03-Customization/07-alert.md b/adminforth/documentation/docs/tutorial/03-Customization/07-alert.md index fecd4d492..9741e1bff 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/07-alert.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/07-alert.md @@ -1,3 +1,7 @@ +--- +description: "Guide to using the Frontend API for alerts, confirmations, and announcement banners inside custom AdminForth pages and Vue components." +--- + # Alerts and confirmations When you are writing custom components or pages you might need to show alerts or confirmations to the user. diff --git a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md index 950ae4a98..d57fae858 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md @@ -1,3 +1,6 @@ +--- +description: "Guide to injecting custom Vue UI into AdminForth login, list, show, edit, and create pages, including dropdown items, custom action icons, and row replacements." +--- # Page Injections diff --git a/adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md b/adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md index 8bfa5cf12..5eb502171 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md @@ -1,3 +1,7 @@ +--- +description: "Guide to defining AdminForth actions for single records and bulk flows, including UI options, handlers, confirmation dialogs, and custom execution logic." +--- + # Actions ## Single record actions @@ -163,9 +167,12 @@ Instead of defining an `action` handler, you can specify a `url` that the user w } ``` +> ☝️ Note: You cannot specify both `action` and `url` for the same action - only one should be used. + The URL can be: - A relative path within your admin panel (starting with '/') - An absolute URL (starting with 'http://' or 'https://') +- function which creates URL based on record fields To open the URL in a new tab, append `target=_blank` as a query parameter. If the URL already has query parameters, use `&target=_blank`; otherwise use `?target=_blank`: @@ -181,7 +188,46 @@ To open the URL in a new tab, append `target=_blank` as a query parameter. If th } ``` -> ☝️ Note: You cannot specify both `action` and `url` for the same action - only one should be used. +Example to generate dynamic URL: + +```ts +{ + name: 'View on Google', + icon: 'flowbite:external-link-solid', + url: async ({record, recordId, adminUser, resource }) => `https://google.com/search?q=Apartment ${record.title}`, + showIn: { + list: true, + showButton: true + } +} +``` + +> ☝️ Note: Though url function might be async we recommend to omit long awaits, or ideally don't use them at all, cause slow execution of this hook might be a subject of bottleneck for resource pages rendering. For built actions the async functions would be called in parallel to optimize loading speed. + + +### Deep-level redirects. + +Using `url` prop described above is recommended way to implementing URL navigation from actions (internal or external), because URLs are rendered into direct anchour tag and support all anchour features (like Open in new tab). + +However, rearely you might also like to decide whether to redirect only after performing some logic (conditionally). This way is not recommended for most of cases, because it is not compatible with action native features (we can't know URL before executing action body): + + +```ts +{ + name: 'View on Google', + icon: 'flowbite:external-link-solid', + action: async ({ recordId }) => { + if (await testSomething(recordId)) { + return { ok: true, redirectUrl: 'https://google.com/search?q=apartment' }; + }; + return { ok: true, successMessage: 'Done' }; + }, + showIn: { + list: true, + showButton: true + } +} +``` ## Custom Component @@ -313,4 +359,4 @@ Backend handler: read the payload via `extra`. Notes: - If you don’t emit a payload, the default behavior is used by the UI (e.g., in lists the current row context is used). When you do provide a payload, it will be forwarded to the backend as `extra` for your action handler. -- You can combine default context with your own payload by merging before emitting, for example: `emit('callAction', { ...row, asListed: true })` if your component has access to the row object. \ No newline at end of file +- You can combine default context with your own payload by merging before emitting, for example: `emit('callAction', { ...row, asListed: true })` if your component has access to the row object. diff --git a/adminforth/documentation/docs/tutorial/03-Customization/10-menuConfiguration.md b/adminforth/documentation/docs/tutorial/03-Customization/10-menuConfiguration.md index 6b597f980..cafaa69e0 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/10-menuConfiguration.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/10-menuConfiguration.md @@ -1,3 +1,7 @@ +--- +description: "Guide to configuring the AdminForth sidebar and header, including icons, grouping, visibility rules, gaps, dividers, and custom links." +--- + # Menu & Header diff --git a/adminforth/documentation/docs/tutorial/03-Customization/11-dataApi.md b/adminforth/documentation/docs/tutorial/03-Customization/11-dataApi.md index 32e4c863a..fe3bec195 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/11-dataApi.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/11-dataApi.md @@ -1,3 +1,7 @@ +--- +description: "Guide to the AdminForth Data API for querying, creating, updating, and deleting records programmatically, including filters, sorting, aggregations, and raw queries." +--- + # Data API AdminForth Data API is a minimal set of methods to manipulate the data in the database. @@ -280,4 +284,89 @@ Create INDEX is just for example, you have to use your migrator / ORM to create First one covers performance for the first query, second one for the second query. If you did not understand how indexes are created: **get sorted tuple of all fields in filters + all fields in sort, -in order they appear in filters and sort**. \ No newline at end of file +in order they appear in filters and sort**. + +## Get aggregated data from database +The aggregate method allows you to compute statistical summaries over database records instead of returning raw rows. It is useful for building analytics, dashboards, charts, and reporting endpoints. + +You can combine: +- filters (to narrow down dataset) +- aggregates (to compute metrics like count, average, sum, median) +- grouping (to split results by field or time periods) +This lets you answer questions like: +- How many apartments are listed per day? +- What is the average price per country? +- What is the total revenue per category? + +### Available aggregates +- Aggregates.count() +- Aggregates.avg(field) +- Aggregates.sum(field) +- Aggregates.median(field) + +### Available grouping +- GroupBy.Field(field) +- GroupBy.DateTrunc(field, unit, timezone) + +Example: +```ts +GroupBy.DateTrunc('created_at', 'month', 'Europe/Kyiv') +``` + +### Response format +```ts +[ + { + group: string, + count?: number | string, + avgPrice?: number | null, + sum?: number | null, + medianPrice?: number | null, + } +] +``` + +### Get daily apartment stats (count, avg, sum, median) for listed apartments +```ts +const rows = await admin.resource('apartments').aggregate( + Filters.EQ('listed', true), + { + count: Aggregates.count(), + avgPrice: Aggregates.avg('price'), + sum: Aggregates.sum('price'), + medianPrice: Aggregates.median('price'), + }, + GroupBy.DateTrunc('created_at', 'day', 'Europe/Kyiv'), +); +``` + +What’s happening here: +- Filters.EQ('listed', true) +→ only apartments that are listed (listed = true) +- aggregates: +count() → number of records in each group +avg('price') → average price +sum('price') → total price +median('price') → median price +- GroupBy.DateTrunc('created_at', 'day', 'Europe/Kyiv') +→ groups data by day (with timezone applied) + +### Get apartment stats grouped by country +```ts +const rows = await admin.resource('apartments').aggregate( + [], + { + count: Aggregates.count(), + avgPrice: Aggregates.avg('price'), + sum: Aggregates.sum('price'), + medianPrice: Aggregates.median('price'), + }, + GroupBy.Field('country'), +); +``` + +What is happening here: +- [] → no filters (all records) +- GroupBy.Field('country') +→ grouping by country +- same aggregates (count, avg, sum, median) \ No newline at end of file diff --git a/adminforth/documentation/docs/tutorial/03-Customization/12-security.md b/adminforth/documentation/docs/tutorial/03-Customization/12-security.md index fc1bed629..8ab29745f 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/12-security.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/12-security.md @@ -1,3 +1,7 @@ +--- +description: "Guide to AdminForth security settings, including session lifetime, password policies, trusted proxy configuration, HTTPS, CSRF scope, and hardening recommendations." +--- + # Security Security and privacy if adminforth users is one of the most important aspects of AdminForth. diff --git a/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md b/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md index 617b8afbc..169fcb967 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md @@ -1,3 +1,6 @@ +--- +description: "Guide to tuning standard AdminForth list, show, edit, and create pages with field groups, sorting, sticky columns, conditional display, defaults, and layout options." +--- # Standard pages tuning diff --git a/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md b/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md index 3061094fb..83c490dbd 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md @@ -1,5 +1,5 @@ --- -description: "AFCL is a set of components which you can use as build blocks in your AdminForth application. AFCL allows to keep the design consistent with minimal efforts and build new pages faster. AFCL components follow styling standard and respect theme colors." +description: "Reference page for the AdminForth Components Library, covering reusable UI components such as buttons, links, badges, inputs, tables, and other building blocks." image: "/ogs/afcl.png" # Path to the OG image --- diff --git a/adminforth/documentation/docs/tutorial/03-Customization/16-websocket.md b/adminforth/documentation/docs/tutorial/03-Customization/16-websocket.md index e61d53ac5..7363e5bd5 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/16-websocket.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/16-websocket.md @@ -1,8 +1,13 @@ +--- +description: "Guide to AdminForth WebSocket usage, including client subscriptions, publish authorization, initial loading, and real-time updates for custom pages or components." +--- # Websocket AdminForth provide own build-in websocket interface which allows to stream some data to frontend from backend. +If in production you run through a reverse proxy such as Nginx, ensure websocket upgrade is enabled on the `/afws` path under your AdminForth base URL, otherwise websocket will not work. See [Deploy in Docker > Nginx version](/docs/tutorial/deploy#nginx-version). + In two words, to subscribe to a topic from any frontend component you need to do next ```javascript diff --git a/adminforth/documentation/docs/tutorial/04-deploy.md b/adminforth/documentation/docs/tutorial/04-deploy.md index 5740c46db..4257dbca7 100644 --- a/adminforth/documentation/docs/tutorial/04-deploy.md +++ b/adminforth/documentation/docs/tutorial/04-deploy.md @@ -1,3 +1,7 @@ +--- +description: "Guide to deploying AdminForth in Docker, including Dockerfile setup, image builds, CI automation, SSL termination, and SQLite-specific notes." +--- + # Deploy in Docker In general you can already run your `index.ts` file which we created in [Getting Started](/docs/tutorial/001-gettingStarted.md) @@ -115,13 +119,18 @@ docker compose -p stack-my-app -f compose.yml up -d --build --remove-orphans --w If you want to deploy your AdminForth application to a sub-folder like `https://mydomain.com/admin` you should do the following: -1) Open `index.ts` file and change `ADMIN_BASE_URL` constant to your subpath: +1) Open `index.ts` file and set the same subpath in `ADMIN_BASE_URL` and `baseUrl`: ```ts title='./index.ts' //diff-remove const ADMIN_BASE_URL = ''; //diff-add const ADMIN_BASE_URL = '/admin/'; + +export const admin = new AdminForth({ + baseUrl: ADMIN_BASE_URL, + ... +}); ``` 2) Open `compose.yml` file and change `traefik.http.routers.adminforth.rule` to your subpath: @@ -141,11 +150,18 @@ Now you can access your AdminForth application by going to `https://mydomain.com If you want to automate the deployment process with CI follow [our docker - traefik guide](https://devforth.io/blog/onlogs-open-source-simplified-web-logs-viewer-for-dockers/) -# Nginx version +## Nginx version -If you are using Nginx instead of traefik, here is siple proxy pass config: +If you are using Nginx instead of traefik, proxy both regular HTTP traffic and AdminForth websocket endpoint. -``` +AdminForth websocket always uses `/afws`, where `` is the same value you pass to `baseUrl: ADMIN_BASE_URL` in `index.ts`. + +- If `ADMIN_BASE_URL = ''`, use `/afws`. +- If `ADMIN_BASE_URL = '/admin/'`, use `/admin/afws`. + +For root deployment, the config can look like this: + +```nginx server { listen 80; server_name demo.adminforth.dev; @@ -163,6 +179,17 @@ server { gzip_min_length 2000; gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon; + location /afws { + proxy_http_version 1.1; + proxy_read_timeout 220s; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://127.0.0.1:3500; + } + location / { proxy_read_timeout 220s; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -173,6 +200,35 @@ server { } ``` +If you deploy AdminForth under a subpath, use the same prefix in Nginx locations. For example, when `ADMIN_BASE_URL = '/admin/'`, the websocket path becomes `/admin/afws`: + +```nginx +server { + ... + + location /admin/afws { + proxy_http_version 1.1; + proxy_read_timeout 220s; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://127.0.0.1:3500; + } + + location /admin/ { + proxy_read_timeout 220s; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://127.0.0.1:3500; + } +} +``` + +If websocket upgrade is not configured on the correct `/afws` path, realtime AdminForth features will not work behind Nginx. + # Environment variables best practices Use `.env` file for sensitive variables like `OPENAI_API_KEY` locally. diff --git a/adminforth/documentation/docs/tutorial/05-Adapters/01-email-adapters.md b/adminforth/documentation/docs/tutorial/05-Adapters/01-email-adapters.md index c92a64d58..81255f42f 100644 --- a/adminforth/documentation/docs/tutorial/05-Adapters/01-email-adapters.md +++ b/adminforth/documentation/docs/tutorial/05-Adapters/01-email-adapters.md @@ -1,3 +1,7 @@ +--- +description: "Reference page for AdminForth email adapters, with setup instructions for AWS SES and Mailgun integrations used by plugins that send transactional email." +--- + # Email Adapters Used to send emails. diff --git a/adminforth/documentation/docs/tutorial/05-Adapters/02-oauth2-adapters.md b/adminforth/documentation/docs/tutorial/05-Adapters/02-oauth2-adapters.md index 8abe31772..3bf267606 100644 --- a/adminforth/documentation/docs/tutorial/05-Adapters/02-oauth2-adapters.md +++ b/adminforth/documentation/docs/tutorial/05-Adapters/02-oauth2-adapters.md @@ -1,3 +1,7 @@ +--- +description: "Reference page for AdminForth OAuth2 adapters, including provider setup for Google, GitHub, Facebook, Keycloak, Microsoft, and Twitch sign-in flows." +--- + # OAuth2 Adapters Used to authenticate users via OAuth 2.0 providers. diff --git a/adminforth/documentation/docs/tutorial/05-Adapters/03-image-generation-adapters.md b/adminforth/documentation/docs/tutorial/05-Adapters/03-image-generation-adapters.md index db3874591..32ffb43e7 100644 --- a/adminforth/documentation/docs/tutorial/05-Adapters/03-image-generation-adapters.md +++ b/adminforth/documentation/docs/tutorial/05-Adapters/03-image-generation-adapters.md @@ -1,3 +1,7 @@ +--- +description: "Reference page for AdminForth image generation adapters, including OpenAI and Gemini integrations used by plugins that create images from prompts." +--- + # Image Generation Adapters Used for image-generating AI tools. diff --git a/adminforth/documentation/docs/tutorial/05-Adapters/04-storage-adapters.md b/adminforth/documentation/docs/tutorial/05-Adapters/04-storage-adapters.md index b1ed5ecc3..b3949f739 100644 --- a/adminforth/documentation/docs/tutorial/05-Adapters/04-storage-adapters.md +++ b/adminforth/documentation/docs/tutorial/05-Adapters/04-storage-adapters.md @@ -1,3 +1,7 @@ +--- +description: "Reference page for AdminForth storage adapters, covering Amazon S3 and local file storage backends used by upload and media workflows." +--- + # Storage Adapters Used for storing files. diff --git a/adminforth/documentation/docs/tutorial/05-Adapters/05-ai-completion-adapters.md b/adminforth/documentation/docs/tutorial/05-Adapters/05-ai-completion-adapters.md index 996860ec0..7385fd152 100644 --- a/adminforth/documentation/docs/tutorial/05-Adapters/05-ai-completion-adapters.md +++ b/adminforth/documentation/docs/tutorial/05-Adapters/05-ai-completion-adapters.md @@ -1,3 +1,7 @@ +--- +description: "Reference page for AdminForth AI completion adapters, including the shared completion interface, structured output, streaming, reasoning events, and provider-specific setup." +--- + # AI Completion Adapters Used for AI-powered text completion. diff --git a/adminforth/documentation/docs/tutorial/05-Adapters/06-image-analysis-adapters.md b/adminforth/documentation/docs/tutorial/05-Adapters/06-image-analysis-adapters.md index 8fa27ba6e..9b38f1d46 100644 --- a/adminforth/documentation/docs/tutorial/05-Adapters/06-image-analysis-adapters.md +++ b/adminforth/documentation/docs/tutorial/05-Adapters/06-image-analysis-adapters.md @@ -1,3 +1,7 @@ +--- +description: "Reference page for AdminForth image analysis adapters, covering OpenAI Vision setup for plugins that read or describe image content." +--- + # Image Analysis Adapters Used for AI-powered image analysis. diff --git a/adminforth/documentation/docs/tutorial/05-Adapters/07-key-value-adapters.md b/adminforth/documentation/docs/tutorial/05-Adapters/07-key-value-adapters.md index 8f71f6119..ece0c9f9d 100644 --- a/adminforth/documentation/docs/tutorial/05-Adapters/07-key-value-adapters.md +++ b/adminforth/documentation/docs/tutorial/05-Adapters/07-key-value-adapters.md @@ -1,3 +1,7 @@ +--- +description: "Reference page for AdminForth key-value adapters, including RAM, Redis, and LevelDB backends for temporary state, caching, and plugin storage." +--- + # Key-value Adapters Key-value adapters are used to store data in a key-value format. They provide a simple and efficient way to manage data where quick access to values based on unique keys is required. diff --git a/adminforth/documentation/docs/tutorial/05-Adapters/08-captcha-adapters.md b/adminforth/documentation/docs/tutorial/05-Adapters/08-captcha-adapters.md index b63c442b5..5ae9c90a3 100644 --- a/adminforth/documentation/docs/tutorial/05-Adapters/08-captcha-adapters.md +++ b/adminforth/documentation/docs/tutorial/05-Adapters/08-captcha-adapters.md @@ -1,3 +1,7 @@ +--- +description: "Reference page for AdminForth captcha adapters, including Cloudflare Turnstile and Google reCAPTCHA setup for login protection flows." +--- + # Captcha Adapters Used to add captcha to the login screen. diff --git a/adminforth/documentation/docs/tutorial/06-CLICommands.md b/adminforth/documentation/docs/tutorial/06-CLICommands.md index af89e3bfd..26e70f87d 100644 --- a/adminforth/documentation/docs/tutorial/06-CLICommands.md +++ b/adminforth/documentation/docs/tutorial/06-CLICommands.md @@ -1,3 +1,7 @@ +--- +description: "Reference page for the AdminForth CLI, including create-app, create-plugin, generate-models, bundle, and other commands used to scaffold and maintain projects." +--- + # CLI Commands > **CLI Reference**: A list of all available CLI commands for AdminForth. diff --git a/adminforth/documentation/docs/tutorial/07-UsageOfLogger.md b/adminforth/documentation/docs/tutorial/07-UsageOfLogger.md index ed2b828a8..94e5f51d5 100644 --- a/adminforth/documentation/docs/tutorial/07-UsageOfLogger.md +++ b/adminforth/documentation/docs/tutorial/07-UsageOfLogger.md @@ -1,3 +1,7 @@ +--- +description: "Guide to the AdminForth logger, including custom log messages, log levels, and SPA debugging for frontend troubleshooting." +--- + # Usage of the logger There are cases when you might want to debug an Adminforth app or add custom logs. diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/01-agent.md b/adminforth/documentation/docs/tutorial/08-Plugins/01-agent.md index 62be9f49c..4f82c4920 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/01-agent.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/01-agent.md @@ -1,5 +1,6 @@ --- title: Agent +description: "Guide to the AdminForth Agent plugin, including installation, model and mode configuration, self-hosted usage, session storage, skills, and custom tools." slug: /tutorial/Plugins/agent sidebar_position: 0 --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/02-TwoFactorsAuth.md b/adminforth/documentation/docs/tutorial/08-Plugins/02-TwoFactorsAuth.md index 038c211f7..232bafce0 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/02-TwoFactorsAuth.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/02-TwoFactorsAuth.md @@ -1,5 +1,6 @@ --- title: Two-Factor Authentication Plugin +description: "Guide to the Two-Factor Authentication plugin, including installation, TOTP or passkey setup, rollout rules, and per-user exceptions for stronger admin login security." slug: /tutorial/Plugins/two-factors-auth --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/03-ForeignInlineList.md b/adminforth/documentation/docs/tutorial/08-Plugins/03-ForeignInlineList.md index 9512d471c..194163d04 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/03-ForeignInlineList.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/03-ForeignInlineList.md @@ -1,5 +1,6 @@ --- title: Foreign Inline List +description: "Guide to the Foreign Inline List plugin, which embeds related resource lists inside a show page with filters, foreign-key setup, and cascade-delete options." slug: /tutorial/Plugins/foreign-inline-list --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/04-AuditLog.md b/adminforth/documentation/docs/tutorial/08-Plugins/04-AuditLog.md index 1511ef32d..a2e92b446 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/04-AuditLog.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/04-AuditLog.md @@ -1,5 +1,6 @@ --- title: Audit Log +description: "Guide to the Audit Log plugin, including installation, log table setup, resource wiring, and tracking of admin changes, custom actions, and client metadata." slug: /tutorial/Plugins/audit-log --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/05-0-upload.md b/adminforth/documentation/docs/tutorial/08-Plugins/05-0-upload.md index 992f50c43..cc367f7aa 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/05-0-upload.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/05-0-upload.md @@ -1,5 +1,6 @@ --- title: Upload +description: "Guide to the Upload plugin, including storage adapter setup, file and image fields, previews, transformations, and image generation integration." slug: /tutorial/Plugins/upload --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/05-1-upload-api.md b/adminforth/documentation/docs/tutorial/08-Plugins/05-1-upload-api.md index 2c676fd63..57654772a 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/05-1-upload-api.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/05-1-upload-api.md @@ -1,3 +1,7 @@ +--- +description: "Reference page for the Upload plugin API, covering backend buffer uploads, uploads to new or existing records, and presigned browser upload flows." +--- + # Upload API The Upload plugin exposes an API for both backend-only uploads and direct browser uploads using presigned URLs. You can: diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/06-markdown.md b/adminforth/documentation/docs/tutorial/08-Plugins/06-markdown.md index 3069c66b7..3e8eacf35 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/06-markdown.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/06-markdown.md @@ -1,5 +1,6 @@ --- title: Markdown +description: "Guide to the Markdown plugin, including editor setup, image handling, top-panel customization, and show-page rendering options." slug: /tutorial/Plugins/markdown --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/07-email-password-reset.md b/adminforth/documentation/docs/tutorial/08-Plugins/07-email-password-reset.md index 775abe8ff..d99221cc2 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/07-email-password-reset.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/07-email-password-reset.md @@ -1,5 +1,6 @@ --- title: Email Password Reset +description: "Guide to the Email Password Reset plugin, including SES or Mailgun setup, reset-flow wiring, and password reset page customization." slug: /tutorial/Plugins/email-password-reset --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/08-import-export.md b/adminforth/documentation/docs/tutorial/08-Plugins/08-import-export.md index 89feb8d99..ccfb53387 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/08-import-export.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/08-import-export.md @@ -1,5 +1,6 @@ --- title: Import Export +description: "Guide to the Import-Export plugin for CSV-based data transfer, including installation, import flow, export flow, and resource-level usage." slug: /tutorial/Plugins/import-export --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/09-open-signup.md b/adminforth/documentation/docs/tutorial/08-Plugins/09-open-signup.md index 3f1a30acd..4492b9375 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/09-open-signup.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/09-open-signup.md @@ -1,5 +1,6 @@ --- title: Open Signup +description: "Guide to the Open Signup plugin, including public registration flow, installation, basic setup, and optional email verification." slug: /tutorial/Plugins/open-signup --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/10-i18n.md b/adminforth/documentation/docs/tutorial/08-Plugins/10-i18n.md index 60c486ea9..534d70dcb 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/10-i18n.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/10-i18n.md @@ -1,5 +1,6 @@ --- title: Internationalization (i18n) +description: "Guide to the Internationalization plugin, including installation, primary language configuration, translated fields, and support for custom components or external apps." slug: /tutorial/Plugins/i18n --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/11-oauth.md b/adminforth/documentation/docs/tutorial/08-Plugins/11-oauth.md index bae3cbbfb..9860b0b33 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/11-oauth.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/11-oauth.md @@ -1,5 +1,6 @@ --- title: OAuth Authentication +description: "Guide to the OAuth Authentication plugin, including provider setup, plugin configuration, email confirmation, and login flows for external identity providers." slug: /tutorial/Plugins/oauth --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/12-inline-create.md b/adminforth/documentation/docs/tutorial/08-Plugins/12-inline-create.md index e853e09a2..043c36ce3 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/12-inline-create.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/12-inline-create.md @@ -1,5 +1,6 @@ --- title: Inline Create +description: "Guide to the Inline Create plugin, which lets users create records directly from the list page and configure which resources use the inline form." slug: /tutorial/Plugins/inline-create --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/13-list-in-place-edit.md b/adminforth/documentation/docs/tutorial/08-Plugins/13-list-in-place-edit.md index 8dd8c6af0..3525d9c48 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/13-list-in-place-edit.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/13-list-in-place-edit.md @@ -1,5 +1,6 @@ --- title: List In-Place Edit +description: "Guide to the List In-Place Edit plugin, which makes selected list fields editable directly in the table without opening the edit page." slug: /tutorial/Plugins/list-in-place-edit --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/14-text-complete.md b/adminforth/documentation/docs/tutorial/08-Plugins/14-text-complete.md index d78f6e185..a1cd319ac 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/14-text-complete.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/14-text-complete.md @@ -1,5 +1,6 @@ --- title: Text Complete +description: "Guide to the Text Complete plugin, including LLM adapter setup, prompt configuration, and AI-assisted text generation for resource fields." slug: /tutorial/Plugins/text-complete --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/15-ForeignInlineShow.md b/adminforth/documentation/docs/tutorial/08-Plugins/15-ForeignInlineShow.md index f256407e2..416d0e76a 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/15-ForeignInlineShow.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/15-ForeignInlineShow.md @@ -1,5 +1,6 @@ --- title: Foreign Inline Show +description: "Guide to the Foreign Inline Show plugin, which embeds a related record show view inside another resource's show page using foreign-key relationships." slug: /tutorial/Plugins/foreign-inline-show --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/16-email-invite.md b/adminforth/documentation/docs/tutorial/08-Plugins/16-email-invite.md index 23d0c56d4..ac5f0b126 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/16-email-invite.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/16-email-invite.md @@ -1,5 +1,6 @@ --- title: Email Invite +description: "Guide to the Email Invite plugin, including SES or Mailgun setup, invitation sending flows, email confirmation handling, and basic usage." slug: /tutorial/Plugins/email-invite --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/17-bulk-ai-flow.md b/adminforth/documentation/docs/tutorial/08-Plugins/17-bulk-ai-flow.md index 75e9cc644..7eabd42f9 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/17-bulk-ai-flow.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/17-bulk-ai-flow.md @@ -1,5 +1,6 @@ --- title: Bulk AI Flow +description: "Guide to the Bulk AI Flow plugin, including text and vision modes, prompt configuration, per-record context, editable prompts, and batch content generation." slug: /tutorial/Plugins/bulk-ai-flow --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/18-universal-filters.md b/adminforth/documentation/docs/tutorial/08-Plugins/18-universal-filters.md index 302303727..86a5f6599 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/18-universal-filters.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/18-universal-filters.md @@ -1,5 +1,6 @@ --- title: Universal Search (Legacy) +description: "Guide to the legacy Universal Search plugin, including installation, options, debounce behavior, and how it searches across configured fields." slug: /tutorial/Plugins/universal-search --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/19-login-captcha.md b/adminforth/documentation/docs/tutorial/08-Plugins/19-login-captcha.md index 93ca792b0..a2791b817 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/19-login-captcha.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/19-login-captcha.md @@ -1,5 +1,6 @@ --- title: Login Captcha +description: "Guide to the Login Captcha plugin, including adapter setup, login form protection, and captcha-verified authentication flow." slug: /tutorial/Plugins/login-captcha --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/20-user-soft-delete.md b/adminforth/documentation/docs/tutorial/08-Plugins/20-user-soft-delete.md index 31b020531..fde6698c6 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/20-user-soft-delete.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/20-user-soft-delete.md @@ -1,5 +1,6 @@ --- title: User Soft Delete +description: "Guide to the User Soft Delete plugin, including schema changes, setup, and deactivating admin users without permanently deleting their accounts." slug: /tutorial/Plugins/user-soft-delete --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/21-clone-row.md b/adminforth/documentation/docs/tutorial/08-Plugins/21-clone-row.md index 83ec25694..c35087e5a 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/21-clone-row.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/21-clone-row.md @@ -1,5 +1,6 @@ --- title: Clone Row +description: "Guide to the Clone Row plugin, which opens the create flow with values copied from an existing record so users can duplicate rows quickly." slug: /tutorial/Plugins/clone-row --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/22-many2many.md b/adminforth/documentation/docs/tutorial/08-Plugins/22-many2many.md index bc4ff3124..4e9bd973f 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/22-many2many.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/22-many2many.md @@ -1,5 +1,6 @@ --- title: Many to Many +description: "Guide to the Many2Many plugin, including junction-table setup, editable relationships on both resources, and cleanup behavior for related records." slug: /tutorial/Plugins/many2many --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/23-background-jobs.md b/adminforth/documentation/docs/tutorial/08-Plugins/23-background-jobs.md index eb70c7888..8d397edcb 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/23-background-jobs.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/23-background-jobs.md @@ -1,5 +1,6 @@ --- title: Background Jobs +description: "Guide to the Background Jobs plugin, including job setup, execution, UI monitoring, custom state rendering, and frontend APIs for job details." slug: /tutorial/Plugins/background-jobs --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/24-quick-filters.md b/adminforth/documentation/docs/tutorial/08-Plugins/24-quick-filters.md index f6c77e78c..8f806a645 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/24-quick-filters.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/24-quick-filters.md @@ -1,5 +1,6 @@ --- title: Quick Filters +description: "Guide to the Quick Filters plugin, which adds one-click predefined filters or search inputs to list pages for faster navigation across common record subsets." slug: /tutorial/Plugins/quick-filters --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/25-auto-remove.md b/adminforth/documentation/docs/tutorial/08-Plugins/25-auto-remove.md index 7027165a9..dcdfea24c 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/25-auto-remove.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/25-auto-remove.md @@ -1,5 +1,6 @@ --- title: Auto Remove Plugin +description: "Guide to the Auto Remove plugin, including count-based and time-based cleanup rules for logs, temporary records, demo data, and other disposable resources." slug: /tutorial/Plugins/auto-remove --- diff --git a/adminforth/documentation/docs/tutorial/08-Plugins/26-RichEditor.md b/adminforth/documentation/docs/tutorial/08-Plugins/26-RichEditor.md index c439f37b6..0cf31b642 100644 --- a/adminforth/documentation/docs/tutorial/08-Plugins/26-RichEditor.md +++ b/adminforth/documentation/docs/tutorial/08-Plugins/26-RichEditor.md @@ -1,5 +1,6 @@ --- title: Rich Editor +description: "Guide to the Rich Editor plugin, including editor setup, multiple editors in one resource, completion features, and image handling in rich-text fields." slug: /tutorial/Plugins/rich-editor --- diff --git a/adminforth/documentation/docs/tutorial/09-Advanced/01-plugin-development.md b/adminforth/documentation/docs/tutorial/09-Advanced/01-plugin-development.md index 66f679c89..b4fbe7f3d 100644 --- a/adminforth/documentation/docs/tutorial/09-Advanced/01-plugin-development.md +++ b/adminforth/documentation/docs/tutorial/09-Advanced/01-plugin-development.md @@ -1,3 +1,7 @@ +--- +description: "Guide to developing AdminForth plugins, including plugin concepts, boilerplate, implementation structure, installation, and activation order." +--- + # Plugin development guide Creating a plugin is a powerful way to extend AdminForth functionality. diff --git a/adminforth/documentation/docs/tutorial/09-Advanced/02-working-without-direct-database-connection.md b/adminforth/documentation/docs/tutorial/09-Advanced/02-working-without-direct-database-connection.md index 2be8e4f40..c6eb6a43d 100644 --- a/adminforth/documentation/docs/tutorial/09-Advanced/02-working-without-direct-database-connection.md +++ b/adminforth/documentation/docs/tutorial/09-Advanced/02-working-without-direct-database-connection.md @@ -1,3 +1,7 @@ +--- +description: "Guide to using AdminForth without a direct database connection by exposing resources through custom connectors or API-backed data access." +--- + # Working without direct database connection Out of the box, AdminForth connects directly to your database using one of the supported drivers (PostgreSQL, MySQL, ClickHouse, MongoDB) and executes queries against it. diff --git a/adminforth/documentation/docusaurus.config.ts b/adminforth/documentation/docusaurus.config.ts index d235bc33f..68210cfed 100644 --- a/adminforth/documentation/docusaurus.config.ts +++ b/adminforth/documentation/docusaurus.config.ts @@ -22,7 +22,11 @@ const config: Config = { projectName: 'devforth.github.io', // Usually your repo name. onBrokenLinks: 'throw', - onBrokenMarkdownLinks: 'warn', + markdown: { + hooks: { + onBrokenMarkdownLinks: 'warn', + }, + }, scripts: [ { @@ -99,6 +103,13 @@ const config: Config = { enumMembersFormat: "table", }, ], + [ + 'docusaurus-plugin-llms', + { + generateMarkdownFiles: true, + ignoreFiles: ['api/**'], + }, + ], // [ // '@docusaurus/plugin-sitemap', // { diff --git a/adminforth/documentation/package-lock.json b/adminforth/documentation/package-lock.json index 3ce8b1e39..9cd4ff493 100644 --- a/adminforth/documentation/package-lock.json +++ b/adminforth/documentation/package-lock.json @@ -24,6 +24,7 @@ "@docusaurus/tsconfig": "3.4.0", "@docusaurus/types": "3.4.0", "@types/react": "^19.2.14", + "docusaurus-plugin-llms": "^0.4.0", "docusaurus-plugin-typedoc": "^1.0.1", "react": "^18.3.1", "typedoc": "^0.28.18", @@ -8506,6 +8507,54 @@ "node": ">=6" } }, + "node_modules/docusaurus-plugin-llms": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/docusaurus-plugin-llms/-/docusaurus-plugin-llms-0.4.0.tgz", + "integrity": "sha512-jYlj2HJ5+gu7oJZuJ83Hk8KlB65YlZZ/7UpHXiL7Qr+qpNBkVocmt2Molc6F3HNr5RqcfhWD/98CvgyNztg/ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "gray-matter": "^4.0.3", + "minimatch": "^9.0.3", + "yaml": "^2.8.1" + }, + "engines": { + "node": ">=18.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rachfop" + }, + "peerDependencies": { + "@docusaurus/core": "^3.0.0" + } + }, + "node_modules/docusaurus-plugin-llms/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/docusaurus-plugin-llms/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/docusaurus-plugin-typedoc": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/docusaurus-plugin-typedoc/-/docusaurus-plugin-typedoc-1.4.2.tgz", diff --git a/adminforth/documentation/package.json b/adminforth/documentation/package.json index 1432238e6..7ec87ac6d 100644 --- a/adminforth/documentation/package.json +++ b/adminforth/documentation/package.json @@ -32,6 +32,7 @@ "@docusaurus/tsconfig": "3.4.0", "@docusaurus/types": "3.4.0", "@types/react": "^19.2.14", + "docusaurus-plugin-llms": "^0.4.0", "docusaurus-plugin-typedoc": "^1.0.1", "react": "^18.3.1", "typedoc": "^0.28.18", diff --git a/adminforth/documentation/src/css/custom.css b/adminforth/documentation/src/css/custom.css index 52e9f9b83..680bd393d 100644 --- a/adminforth/documentation/src/css/custom.css +++ b/adminforth/documentation/src/css/custom.css @@ -370,10 +370,81 @@ html[data-theme="dark"] .fakeScreen { margin-bottom: 20px; } + .laptop_container { + margin-bottom: 3rem; + } + .laptop { width: 80vw; } + .laptop::before, + .laptop::after { + position: absolute; + z-index: 2; + border-radius: 999px; + background: linear-gradient(to bottom, #2b313b, #16191f); + content: ""; + } + + .laptop::before { + top: 20%; + right: -0.28rem; + width: 0.22rem; + height: 4rem; + } + + .laptop::after { + top: 28%; + left: -0.28rem; + width: 0.22rem; + height: 2.2rem; + box-shadow: 0 2.8rem 0 0 #16191f; + } + + .laptop .laptop__screen { + padding: 11% 1.5% 6%; + border-radius: 2rem; + border: 0.5rem solid #16191f; + background: linear-gradient(to bottom, #232932, #08090b); + box-shadow: + 0 1.25rem 2.5rem rgba(15, 23, 42, 0.18), + inset 0 0.08rem 0 rgba(255, 255, 255, 0.08); + } + + .laptop .laptop__screen::before { + position: absolute; + top: 0.95rem; + left: 50%; + z-index: 2; + width: 30%; + height: 0.38rem; + transform: translateX(-50%); + border-radius: 999px; + background: rgba(6, 8, 11, 0.92); + box-shadow: inset 0 0.05rem 0 rgba(255, 255, 255, 0.08); + content: ""; + } + + .laptop .laptop__screen iframe { + aspect-ratio: 9 / 19.5; + border-radius: 1rem; + } + + .laptop .laptop__bottom, + .laptop .laptop__under { + display: none; + } + + .laptop .laptop__shadow { + right: 12%; + bottom: -1.25rem; + left: 12%; + height: 1.5rem; + background: radial-gradient(ellipse closest-side, rgba(15, 23, 42, 0.4), transparent 80%); + opacity: 1; + } + .theme_switcher { display: none !important; } diff --git a/adminforth/index.ts b/adminforth/index.ts index 98889dd69..8af6d2941 100644 --- a/adminforth/index.ts +++ b/adminforth/index.ts @@ -51,6 +51,7 @@ export * from './modules/filtersTools.js'; export { interpretResource }; export { AdminForthPlugin }; export { suggestIfTypo, RateLimiter, RAMLock, getClientIp, convertPeriodToSeconds }; +export { default as AdminForthBaseConnector } from './dataConnectors/baseConnector.js'; class AdminForth implements IAdminForth { @@ -417,9 +418,10 @@ class AdminForth implements IAdminForth { 'mysql': MysqlConnector, 'qdrant': QdrantConnector, }; - if (!this.config.databaseConnectors) { - this.config.databaseConnectors = {...this.connectorClasses}; - } + this.config.databaseConnectors = { + ...this.connectorClasses, + ...this.config.databaseConnectors, + }; this.config.dataSources.forEach((ds) => { const dbType = ds.url.split(':')[0]; if (!this.config.databaseConnectors[dbType]) { diff --git a/adminforth/modules/configValidator.ts b/adminforth/modules/configValidator.ts index 5e8637fa2..4e33b83b4 100644 --- a/adminforth/modules/configValidator.ts +++ b/adminforth/modules/configValidator.ts @@ -417,15 +417,18 @@ export default class ConfigValidator implements IConfigValidator { if (!action.id) { action.id = md5hash(action.name); } + + const defaultListValue = !!(action.action || action.url); + if (!action.showIn) { action.showIn = { - list: true, + list: defaultListValue, listThreeDotsMenu: false, showButton: false, showThreeDotsMenu: false, } } else { - action.showIn.list = action.showIn.list ?? true; + action.showIn.list = action.showIn.list ?? defaultListValue; action.showIn.listThreeDotsMenu = action.showIn.listThreeDotsMenu ?? false; action.showIn.showButton = action.showIn.showButton ?? false; action.showIn.showThreeDotsMenu = action.showIn.showThreeDotsMenu ?? false; diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index 5b8001ccd..7c3d7dae5 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -2238,6 +2238,11 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { if (!resource) { return { error: await tr(`Resource {resourceId} not found`, 'errors', { resourceId }) }; } + + const record = await this.adminforth.connectors[resource.dataSource].getRecordByPrimaryKey(resource, recordId); + if (!record){ + return { error: `Record with ${recordId} not found` }; + } const { allowedActions } = await interpretResource( adminUser, resource, @@ -2257,16 +2262,18 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { } if (action.url) { + const redirectUrl = typeof action.url === 'function' + ? await action.url({ record, recordId, adminUser, resource }) + : action.url; return { actionId, recordId, + record, resourceId, - redirectUrl: action.url + redirectUrl, } } - const actionResponse = await action.action({ recordId, adminUser, resource, tr, adminforth: this.adminforth, response, extra: {...extra, cookies: cookies, headers: headers} }); - return { actionId, recordId, diff --git a/adminforth/package-lock.json b/adminforth/package-lock.json index bc3c2f0d4..891890934 100644 --- a/adminforth/package-lock.json +++ b/adminforth/package-lock.json @@ -38,6 +38,7 @@ "jsonwebtoken": "^9.0.2", "listr2": "^8.2.5", "mongodb": "6.6", + "multer": "^2.1.1", "mysql2": "^3.14.2", "node-fetch": "^3.3.2", "pg": "^8.11.5", @@ -1383,6 +1384,12 @@ "dev": true, "license": "MIT" }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -1639,6 +1646,23 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2194,6 +2218,35 @@ "dot-prop": "^5.1.0" } }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -4883,6 +4936,25 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -9516,6 +9588,14 @@ "readable-stream": "^2.0.2" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -9972,6 +10052,12 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/adminforth/package.json b/adminforth/package.json index eb3759acf..3b495c163 100644 --- a/adminforth/package.json +++ b/adminforth/package.json @@ -112,6 +112,7 @@ "jsonwebtoken": "^9.0.2", "listr2": "^8.2.5", "mongodb": "6.6", + "multer": "^2.1.1", "mysql2": "^3.14.2", "node-fetch": "^3.3.2", "pg": "^8.11.5", diff --git a/adminforth/pnpm-lock.yaml b/adminforth/pnpm-lock.yaml index ebe69bf16..68245b67f 100644 --- a/adminforth/pnpm-lock.yaml +++ b/adminforth/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: mongodb: specifier: '6.6' version: 6.6.2 + multer: + specifier: ^2.1.1 + version: 2.1.1 mysql2: specifier: ^3.14.2 version: 3.19.0(@types/node@20.19.37) @@ -1478,6 +1481,9 @@ packages: apexcharts@4.7.0: resolution: {integrity: sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==} + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -1592,9 +1598,16 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1733,6 +1746,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -2934,6 +2951,10 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} + engines: {node: '>= 10.16.0'} + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -3781,6 +3802,10 @@ packages: stream-combiner2@1.1.1: resolution: {integrity: sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3972,6 +3997,9 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} @@ -5436,6 +5464,8 @@ snapshots: '@svgdotjs/svg.select.js': 4.0.3(@svgdotjs/svg.js@3.2.5) '@yr/monotone-cubic-spline': 1.0.3 + append-field@1.0.0: {} + arg@5.0.2: {} argparse@2.0.1: {} @@ -5550,11 +5580,17 @@ snapshots: buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + bytes@3.1.2: {} cac@6.7.14: {} @@ -5693,6 +5729,13 @@ snapshots: concat-map@0.0.1: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -6917,6 +6960,13 @@ snapshots: muggle-string@0.4.1: {} + multer@2.1.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + mute-stream@2.0.0: {} mysql2@3.19.0(@types/node@20.19.37): @@ -7772,6 +7822,8 @@ snapshots: duplexer2: 0.1.4 readable-stream: 2.3.8 + streamsearch@1.1.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -7978,6 +8030,8 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + typedarray@0.0.6: {} + typescript@5.4.5: {} typescript@5.9.3: {} diff --git a/adminforth/servers/express.ts b/adminforth/servers/express.ts index 301d48d38..214689898 100644 --- a/adminforth/servers/express.ts +++ b/adminforth/servers/express.ts @@ -15,6 +15,7 @@ import { randomUUID } from 'crypto'; import { listify } from '../modules/utils.js'; import { afLogger } from '../modules/logger.js'; import * as z from 'zod'; +import multer from 'multer'; function replaceAtStart(string, substring) { if (string.startsWith(substring)) { @@ -62,6 +63,7 @@ const EXPRESS_REGEXP_PARAM_CAPTURE_RE = /\(\?:\(\[\^\\\/]\+\?\)\)/g; const EXPRESS_REGEXP_ESCAPED_SLASH_RE = /\\\//g; const EXPRESS_REGEXP_TRAILING_DOLLAR_RE = /\$$/; const EXPRESS_REGEXP_LEADING_CARET_RE = /^\^/; +type MulterParser = (req: any, res: any, callback: (error?: unknown) => void) => void; type RegisteredExpressRouteSchema = IAdminForthExpressRouteSchema & { request?: AnySchemaObject; @@ -154,9 +156,13 @@ class ExpressServer implements IExpressHttpServer { adminforth: IAdminForth; server: http.Server; schemaAwareRouteRegistrationPatched = false; + uploadParser: MulterParser; constructor(adminforth: IAdminForth) { this.adminforth = adminforth; + this.uploadParser = multer({ + storage: multer.memoryStorage(), + }).any(); } setupSpaServer() { @@ -458,7 +464,8 @@ class ExpressServer implements IExpressHttpServer { description: schema.description, request_schema: schema.request, response_schema: schema.response, - handler: async () => null, + meta: schema.meta, + handler: undefined as never, }); }); }); @@ -542,6 +549,7 @@ class ExpressServer implements IExpressHttpServer { request_schema, response_schema, responce_schema, + target='json' } = options; if (!path.startsWith('/')) { throw new Error(`Path must start with /, got: ${path}`); @@ -571,17 +579,39 @@ class ExpressServer implements IExpressHttpServer { // AdminForth API endpoints accept only application/json for POST, PUT, PATCH, DELETE // If you need other content types, use a custom server endpoint. const method = (req.method || '').toUpperCase(); + const contentTypeHeader = (req.headers?.['content-type'] || '').toString(); if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) { - const contentTypeHeader = (req.headers?.['content-type'] || '').toString(); - const isJson = contentTypeHeader.toLowerCase().startsWith('application/json'); - if (!isJson) { + const expectedContentType = target === 'upload' ? 'multipart/form-data' : 'application/json'; + const hasExpectedContentType = contentTypeHeader.toLowerCase().startsWith(expectedContentType); + if (!hasExpectedContentType) { const passed = contentTypeHeader || 'undefined'; - res.status(415).send(`AdminForth API endpoints support only requests with Content/Type: application/json, when you passed: ${passed}. Please use custom server endpoint if you really need this content type`); + res.status(415).send(`AdminForth API endpoint supports only requests with Content-Type: ${expectedContentType}, when you passed: ${passed}. Please use custom server endpoint if you really need this content type`); + return; + } + } + if (target === 'upload') { + try { + await new Promise((resolve, reject) => { + this.uploadParser(req, res, (error?: unknown) => { + if (error) { + reject(error); + return; + } + + resolve(); + }); + }); + if (!(req as any).file && Array.isArray((req as any).files) && (req as any).files.length) { + (req as any).file = (req as any).files[0]; + } + } catch (error) { + afLogger.error(`Failed to parse multipart form-data body, ${error}`); + res.status(400).send('Invalid multipart/form-data body'); return; } } let body = req.body || {}; - if (typeof body === 'string') { + if (typeof body === 'string' && target === 'json') { try { body = JSON.parse(body); } catch (e) { diff --git a/adminforth/servers/openapi.ts b/adminforth/servers/openapi.ts index 481ca3a57..61551a5ac 100644 --- a/adminforth/servers/openapi.ts +++ b/adminforth/servers/openapi.ts @@ -58,6 +58,8 @@ class OpenApiRegistry implements IOpenApiRegistry { description: options.description, request_schema: options.request_schema, response_schema: responseSchema, + meta: options.meta, + handler: options.handler, }; const compiledRoute: CompiledApiSchema = { ...route, @@ -131,4 +133,4 @@ class OpenApiRegistry implements IOpenApiRegistry { } } -export default OpenApiRegistry; \ No newline at end of file +export default OpenApiRegistry; diff --git a/adminforth/spa/src/afcl/Link.vue b/adminforth/spa/src/afcl/Link.vue index 1ca4a7f68..e38129e61 100644 --- a/adminforth/spa/src/afcl/Link.vue +++ b/adminforth/spa/src/afcl/Link.vue @@ -1,17 +1,37 @@ \ No newline at end of file diff --git a/adminforth/spa/src/components/ResourceListTable.vue b/adminforth/spa/src/components/ResourceListTable.vue index 6f84ba445..df90cafc2 100644 --- a/adminforth/spa/src/components/ResourceListTable.vue +++ b/adminforth/spa/src/components/ResourceListTable.vue @@ -585,6 +585,7 @@ async function onClick(e: any, row: any) { async function deleteRecord(row: any) { const data = await confirm({ title: t('Are you sure you want to delete this item?'), + message: t(`This process is irreversible.`), yes: t('Delete'), no: t('Cancel'), }); diff --git a/adminforth/spa/src/components/Sidebar.vue b/adminforth/spa/src/components/Sidebar.vue index df100c351..fb01e1dea 100644 --- a/adminforth/spa/src/components/Sidebar.vue +++ b/adminforth/spa/src/components/Sidebar.vue @@ -32,7 +32,7 @@ {{ coreStore.config?.brandName }} diff --git a/adminforth/spa/src/utils/listUtils.ts b/adminforth/spa/src/utils/listUtils.ts index a916ca701..fef32f33a 100644 --- a/adminforth/spa/src/utils/listUtils.ts +++ b/adminforth/spa/src/utils/listUtils.ts @@ -3,6 +3,7 @@ import { callAdminForthApi } from '@/utils'; import { type AdminForthResourceFrontend } from '../types/Common'; import { useAdminforth } from '@/adminforth'; import { showErrorTost } from '@/composables/useFrontendApi' +import { useI18n } from 'vue-i18n'; let getResourceDataLastAbortController: AbortController | null = null; export async function getList(resource: AdminForthResourceFrontend, isPageLoaded: boolean, page: number | null , pageSize: number, sort: any, checkboxes:{ value: any[] }, filters: any = [] ) { @@ -57,10 +58,12 @@ export async function startBulkAction(actionId: string, resource: AdminForthReso bulkActionLoadingStates: {value: Record}, getListInner: () => Promise) { const action = resource?.options?.bulkActions?.find(a => a.id === actionId); const { confirm, alert } = useAdminforth(); + const { t } = useI18n(); if (action?.confirm) { const confirmed = await confirm({ title: action.confirm, + message: t(`Deleting ${checkboxes.value.length} ${checkboxes.value.length === 1 ? 'item' : 'items'}. This process is irreversible.`), }); if (!confirmed) { return; diff --git a/adminforth/spa/src/views/ShowView.vue b/adminforth/spa/src/views/ShowView.vue index 1990dd662..75985e78d 100644 --- a/adminforth/spa/src/views/ShowView.vue +++ b/adminforth/spa/src/views/ShowView.vue @@ -311,6 +311,7 @@ const otherColumns = computed(() => { async function deleteRecord() { const data = await confirm({ title: t('Are you sure you want to delete this item?'), + message: t(`This process is irreversible.`), yes: t('Delete'), no: t('Cancel'), }); diff --git a/adminforth/types/Back.ts b/adminforth/types/Back.ts index e5bd3dd0d..c4fb15d72 100644 --- a/adminforth/types/Back.ts +++ b/adminforth/types/Back.ts @@ -62,6 +62,8 @@ export interface IAdminForthEndpointOptions { request_schema?: AnySchemaObject, response_schema?: AnySchemaObject, responce_schema?: AnySchemaObject, + meta?: Record, + target?: 'json' | 'upload', handler: (input: IAdminForthEndpointHandlerInput) => void | Promise, } @@ -82,14 +84,21 @@ export interface IAdminForthExpressRouteSchema { * JSON schema or Zod schema describing the JSON response body for a custom Express route. */ response?: AdminForthExpressSchemaInput; + + /** + * Internal metadata for AdminForth integrations. This is not rendered in the OpenAPI document. + */ + meta?: Record; } export interface IRegisteredApiSchema { method: string; path: string; description?: string; + meta?: Record; request_schema?: AnySchemaObject; response_schema?: AnySchemaObject; + handler?: (input: IAdminForthEndpointHandlerInput) => void | Promise; } export interface IAdminForthApiValidationError { @@ -1416,7 +1425,7 @@ export interface AdminForthActionInput { adminUser: AdminUser; standardAllowedActions: AllowedActions; }) => boolean | Promise); - url?: string; + url?: string | ((params: { adminUser: AdminUser; resource: AdminForthResource; recordId: string, record: any }) => string); bulkHandler?: (params: { adminforth: IAdminForth; resource: AdminForthResource; diff --git a/adminforth/types/Common.ts b/adminforth/types/Common.ts index 7d2b8e7af..34828c6c5 100644 --- a/adminforth/types/Common.ts +++ b/adminforth/types/Common.ts @@ -66,7 +66,7 @@ export enum ActionCheckSource { EditRequest = 'editRequest', CreateRequest = 'createRequest', DeleteRequest = 'deleteRequest', - BulkActionRequest = 'bulkActionRequest', + BulkActionRequest = 'bulkActionRequest', // @deprecated beacuse whole bulk action is deprecated in favor of custom actions, never use this value in new code CustomActionRequest = 'customActionRequest', } diff --git a/adminforth/types/adapters/AudioAdapter.ts b/adminforth/types/adapters/AudioAdapter.ts new file mode 100644 index 000000000..5cffd5bda --- /dev/null +++ b/adminforth/types/adapters/AudioAdapter.ts @@ -0,0 +1,73 @@ +export type SpeechToTextInput = { + buffer: Buffer; + filename: string; + mimeType: string; + language?: string; + prompt?: string; +}; + +export type SpeechToTextResult = { + text: string; + language?: string; + raw?: unknown; +}; + +export interface SpeechToTextAdapter { + name: string; + + validate(): void; + + transcribe(input: SpeechToTextInput): Promise; +} + +export type TtsAudioFormat = + | "mp3" + | "opus" + | "aac" + | "flac" + | "wav" + | "pcm"; + +export type TextToSpeechInput = { + text: string; + voice?: Voice; + format?: TtsAudioFormat; + speed?: number; + instructions?: string; + stream?: false; +}; + +export type TextToSpeechResult = { + audio: Buffer; + mimeType: string; + format: TtsAudioFormat; + raw?: unknown; +}; + +export type TtsStreamFormat = "audio" | "sse"; + +export type TextToSpeechStreamInput = + Omit, "stream"> & { + stream: true; + streamFormat?: TtsStreamFormat; + }; + +export type TextToSpeechStreamResult = { + audioStream: ReadableStream; + mimeType: string; + format: TtsAudioFormat; + streamFormat: TtsStreamFormat; + raw?: unknown; +}; + +export interface TextToSpeechAdapter { + name: string; + + validate(): void; + + synthesize(input: TextToSpeechStreamInput): Promise; + synthesize(input: TextToSpeechInput): Promise; +} + +export type AudioAdapter = + SpeechToTextAdapter & TextToSpeechAdapter; diff --git a/adminforth/types/adapters/index.ts b/adminforth/types/adapters/index.ts index f492ce09f..5abb5b2a6 100644 --- a/adminforth/types/adapters/index.ts +++ b/adminforth/types/adapters/index.ts @@ -15,3 +15,13 @@ export type { ImageVisionAdapter } from './ImageVisionAdapter.js'; export type { OAuth2Adapter } from './OAuth2Adapter.js'; export type { StorageAdapter } from './StorageAdapter.js'; export type { CaptchaAdapter } from './CaptchaAdapter.js'; +export type { + AudioAdapter, + SpeechToTextAdapter, + SpeechToTextInput, + SpeechToTextResult, + TextToSpeechAdapter, + TextToSpeechInput, + TextToSpeechResult, + TtsAudioFormat, +} from './AudioAdapter.js'; diff --git a/dev-demo/Taskfile.yaml b/dev-demo/Taskfile.yaml index bbd37693d..456b8a296 100644 --- a/dev-demo/Taskfile.yaml +++ b/dev-demo/Taskfile.yaml @@ -55,6 +55,7 @@ vars: - "adminforth-completion-adapter-openai-responses" - "adminforth-completion-adapter-antropic-messages" - "adminforth-completion-adapter-google-gemini" + - "adminforth-audio-adapter-openai" tasks: diff --git a/dev-demo/api.ts b/dev-demo/api.ts index 5bef32dd0..f39a9fe62 100644 --- a/dev-demo/api.ts +++ b/dev-demo/api.ts @@ -20,7 +20,7 @@ type DashboardCarRecord = { engine_type: string; body_type: string; }; - + function toNumber(value: string | number | null): number { if (typeof value === 'number') { return value; diff --git a/dev-demo/resources/adminuser.ts b/dev-demo/resources/adminuser.ts index d54690c0d..b7ba5a2f9 100644 --- a/dev-demo/resources/adminuser.ts +++ b/dev-demo/resources/adminuser.ts @@ -12,6 +12,7 @@ import OAuthPlugin from '../../plugins/adminforth-oauth/index.js'; import KeyValueAdapterRam from '../../adapters/adminforth-key-value-adapter-ram/index.js'; import AdminForthAgent from '../../plugins/adminforth-agent/index.js'; import CompletionAdapterOpenAIResponses from '../../adapters/adminforth-completion-adapter-openai-responses/index.js'; +import OpenAIAudioAdapter from '../../adapters/adminforth-audio-adapter-openai/index.js'; const OVH_AI_ENDPOINTS_BASE_URL = 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1'; const ovhAiEndpointsAccessToken = process.env.OVH_AI_ENDPOINTS_ACCESS_TOKEN; @@ -215,6 +216,9 @@ export default { }, }), new AdminForthAgent({ + audioAdapter: new OpenAIAudioAdapter({ + apiKey: process.env.OPENAI_API_KEY, + }), placeholderMessages: async ({ adminUser, httpExtra }) => { return [ "What is a cars count in SQLite",