Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ node_modules/
# assets/js is deliberately committed. It is what makes "download the zip from a
# tag and install it" work with no npm install, and CI fails if the committed
# bundles stop matching their TypeScript.

# Browser test output.
/test-results/
/playwright-report/
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,3 +453,52 @@ npm run test:php # 465 PHP tests, inside wp-env
## Licence

GPL-2.0-or-later. Every premium feature, free, forever.

## Export, edit and import forms as YAML

Use **Export** in the builder to download the current form with its base theme, only changed theme/advanced CSS settings, all field/settings/logic/action configuration and embedded image-choice attachments. **Validate YAML** checks a file without writing anything. **Import** accepts YAML or versioned JSON and creates a new draft with its own theme.

For LLM-assisted editing, use the [format guide](docs/form-packages.md), [JSON Schema](schemas/form-package-v1.schema.json) and [example YAML](docs/examples/contact-form.yaml). Validate a document locally with:

```bash
npm run validate:form -- path/to/form.yaml
```

This checks the portable contract offline. Import also checks installed field types, safe theme tokens and image contents. Entries and external plugin/site configuration are separate from the form definition; see the guide for dependency handling.

The browser round-trip test runs against the local wp-env site with OpenStation and AllTerrain Forms active:

```bash
npx playwright install chromium
npm run env:start
npm run test:e2e
```

`ATF_E2E_URL`, `ATF_E2E_USER`, and `ATF_E2E_PASSWORD` override the default local URL (`http://localhost:8889`) and `admin` / `password` test account. The test creates its own form/theme and deletes them afterward. On an older development database, reactivate AllTerrain Forms to refresh its capabilities first.

### MIO form editing

With OpenStation's compatible window MIO API, open AllTerrain Forms and use **Ask
MIO** to create a draft or update the current form. YAML is validated before saving;
invalid candidates receive precise errors and up to two correction attempts.
Existing forms keep their publication status. Manual and file workflows remain
available in classic admin. Provider setup and MIO enablement belong to OpenStation.

- [Detailed linked Markdown knowledge base](docs/mio/index.md)
- [Developer handoff: review of MIO PR #816](docs/mio-api-feedback.md)
- [Conditional contact YAML example](docs/examples/mio-conditional-contact.yaml)

Browser tests default to Docker `http://localhost:8889` (`npm run test:e2e`). The MIO
adapter cases capture the real shell registration and simulate model tool calls.
The chat case uses the real composer, session and rendered Preview button, replacing
only provider responses. All use real WordPress validation/saves without invoking
a paid AI provider.


The recovery MIO API adds turn-wide correction limits, compact document history
and read-only reconciliation of uncertain saves. To exercise the adapter against
an actual local MIO checkout, run
`ATF_MIO_SOURCE=../alcazaba-plugin npm test -- --run tests/vitest/mio-contract.test.ts`.
The [response button handoff](docs/mio-action-buttons-proposal.md) records the plan
and implementation follow-up. Forms supplies a Preview action after a confirmed
save when the shell supports the new optional responseActions API.
2 changes: 2 additions & 0 deletions allterrain-forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@
require_once ALLTFO_DIR . 'includes/importer-wpforms.php';
require_once ALLTFO_DIR . 'includes/importer-gravityforms.php';
require_once ALLTFO_DIR . 'includes/import-notice.php';
require_once ALLTFO_DIR . 'includes/portability.php';
require_once ALLTFO_DIR . 'includes/assistant.php';
require_once ALLTFO_DIR . 'includes/rest.php';
require_once ALLTFO_DIR . 'includes/abilities.php';
require_once ALLTFO_DIR . 'includes/preview.php';
Expand Down
22 changes: 22 additions & 0 deletions assets/css/builder.css
Original file line number Diff line number Diff line change
Expand Up @@ -4410,3 +4410,25 @@ os-checkbox-label.atfb-check {
.atfb-condition-editor input::placeholder {
color: var( --atfb-muted );
}

/* Persistent, copyable results for package validation and import. */
.atfb-package-dialog {
box-sizing: border-box;
inline-size: min( 640px, calc( 100vw - 32px ) );
max-block-size: calc( 100vh - 32px );
padding: 24px;
border: 1px solid var( --atfb-border );
border-radius: 8px;
background: var( --atfb-surface );
color: var( --atfb-fg );
}

.atfb-package-dialog::backdrop {
background: rgba( 0, 0, 0, 0.35 );
}

.atfb-package-dialog pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
font: inherit;
}
16 changes: 13 additions & 3 deletions assets/js/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ var allTerrainFormsAnalytics = function(exports) {
}
const config = window.allTerrainForms;
class ApiError extends Error {
constructor(message, status, code = "") {
constructor(message, status, code = "", data) {
super(message);
this.name = "ApiError";
this.status = status;
this.code = code;
this.data = data;
}
}
function joinPath(base, path) {
Expand Down Expand Up @@ -38,13 +39,15 @@ var allTerrainFormsAnalytics = function(exports) {
if (!response.ok) {
let message = `Request failed with status ${response.status}.`;
let code = "";
let data;
try {
const body = await response.json();
message = body.message ?? message;
code = body.code ?? "";
data = body.data;
} catch {
}
throw new ApiError(message, response.status, code);
throw new ApiError(message, response.status, code, data);
}
if (response.status === 204) {
return void 0;
Expand Down Expand Up @@ -94,6 +97,10 @@ var allTerrainFormsAnalytics = function(exports) {
return form;
}
const api = {
assistantRevision: (id, signal) => request(`/assistant/forms/${id}`, { signal }),
assistantValidate: (draft, signal) => request("/assistant/validate", { method: "POST", body: JSON.stringify({ draft }), signal }),
assistantApply: (draft, formId, revision, signal, operationKey) => request("/assistant/apply", { method: "POST", body: JSON.stringify({ draft, formId, revision, operationKey }), signal }).then((form) => ({ ...withObjectOverrides(form), operation: form.operation })),
assistantOperation: (key, signal) => request(`/assistant/operations/${encodeURIComponent(key)}`, { signal }),
config: () => get("/config"),
/** MailPoet's presence, lists and logo — what the MailPoet window boots from. */
mailpoet: () => get("/mailpoet"),
Expand All @@ -104,9 +111,12 @@ var allTerrainFormsAnalytics = function(exports) {
archiveForm: (id) => post(`/forms/${id}/archive`, {}),
/** Brings an archived form back, entries and stats included, in its pre-archive status. */
unarchiveForm: (id) => post(`/forms/${id}/unarchive`, {}),
getForm: (id) => get(`/forms/${id}`).then(withObjectOverrides),
getForm: (id, signal) => request(`/forms/${id}`, { signal }).then(withObjectOverrides),
createForm: (body) => post("/forms", body).then(withObjectOverrides),
updateForm: (id, body) => post(`/forms/${id}`, body).then(withObjectOverrides),
exportForm: (id, body) => post(`/forms/${id}/export`, body),
validateFormPackage: (value) => post("/form-packages/validate", { package: value }),
importFormPackage: (value) => post("/form-packages/import", { package: value }).then((form) => ({ ...withObjectOverrides(form), importWarnings: form.importWarnings })),
duplicateForm: (id) => post(`/forms/${id}/duplicate`, {}).then(withObjectOverrides),
deleteForm: (id) => del(`/forms/${id}`),
preview: (id, body) => post(`/forms/${id}/preview`, body),
Expand Down
2 changes: 1 addition & 1 deletion assets/js/analytics.min.js

Large diffs are not rendered by default.

Loading
Loading