From 65b46983d4f225a6877064e974682bd42f35b20c Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Thu, 28 May 2026 15:01:47 -0500 Subject: [PATCH] Ensure the skill describes when resources would be used vs forms. Also discusses common anti-patterns that should be avoided. --- plugin/skills/formio-application/INTENT.md | 2 +- plugin/skills/formio-application/SKILL.md | 40 ++++++- .../skills/formio-resource-planner/SKILL.md | 103 ++++++++++++++++-- .../references/template-json.md | 65 ++++++++++- .../references/template-md.md | 26 ++++- 5 files changed, 224 insertions(+), 12 deletions(-) diff --git a/plugin/skills/formio-application/INTENT.md b/plugin/skills/formio-application/INTENT.md index 3df3850..8d1f651 100644 --- a/plugin/skills/formio-application/INTENT.md +++ b/plugin/skills/formio-application/INTENT.md @@ -41,7 +41,7 @@ When the user's phrasing is genuinely ambiguous, ask the question. ### Build-new branch -1. **Step 2 — Plan (full)** — `formio-resource-planner` produces the approved artifact pair `template.md` (architectural intent with Access Matrix + ER and Access Flow diagrams) and `template.json` (full project export with every resource, role, form, and action for the new app). The planner's own Phase A → Phase B gate is the only gate needed. +1. **Step 2 — Plan (full)** — `formio-resource-planner` produces the approved artifact pair `template.md` (architectural intent with Access Matrix + ER and Access Flow diagrams) and `template.json` (full project export with every resource, role, form, and action for the new app). The planner classifies each entity as a Resource (reusable data model) or a bespoke Form (purpose-specific data collection) — see `formio-resource-planner` → "Resources vs. Forms" — so survey-like / one-off intakes become forms, not resources. The planner's own Phase A → Phase B gate is the only gate needed. 2. **Step 3 — Deployment** — URL interview for `FORMIO_BASE_URL` + `FORMIO_PROJECT_URL`. 3. **Step 4 — MCP Config** — writes `./.mcp.json` and halts. User restarts Claude Code (or runs `/mcp` reconnect). Steps 5 & 6 run in the next invocation. 4. **Step 5 — Import** — `project_import` of the full template into the (empty) project. diff --git a/plugin/skills/formio-application/SKILL.md b/plugin/skills/formio-application/SKILL.md index 3f80fb3..5e91d56 100644 --- a/plugin/skills/formio-application/SKILL.md +++ b/plugin/skills/formio-application/SKILL.md @@ -63,6 +63,7 @@ You are the library's default "build me an app" skill. When a user describes an - **Translate, do not interrogate.** Lead with a plain-language restatement of what the app (or the new feature) will DO and let the user confirm or correct. Never open the conversation with Form.io or framework jargon. - **One step at a time, left to right.** Intent → Plan → Deployment → MCP Config → Import → Framework. Each step that writes files, calls the MCP server, or imports into a live project ends with an approval gate. A declined gate stops the flow; partial state is never left behind. - **Route, do not reimplement.** Planning lives in `formio-resource-planner`. Framework file generation lives in `formio-angular` (today) and in future framework skills. Your job is to orchestrate the handoffs, not to duplicate their logic. +- **Pick the right kind per entity — Resource or Form.** Most of what users describe is a reusable **data model** (a Resource — Contact, Product, Project), and many apps are entirely Resources — that is correct and common. Some entities are instead **bespoke data collection** (a Form — a job application, a survey, an RSVP, an intake/feedback form). The planner makes this call per entity; do not force everything into Resources, and equally do not force an entity into a Form when a Resource fits. When the user's request is clearly survey-like or one-off (e.g., "a form for people to apply"), say so in your plain-language restatement so the planner can classify it as a Form. See `formio-resource-planner` → "Resources vs. Forms — the core modeling decision". - **Modify-existing still plans and imports.** If the user is extending an already-running app, still run the planner (in delta mode — it plans ONLY the new resources/fields/actions for the feature) and still call `project_import` (import is additive — adding new resources to the existing project is safe). What modify-existing skips is Deployment (URLs are already in the workspace) and MCP Config (`.mcp.json` already exists and targets the right project). Then route to the framework's extend sub-skill with the new resources in hand. - **Batch your questions.** When input is needed (URLs in Step 3, framework pick in Step 6), use one `AskUserQuestion` per step. Do not pepper. - **Expect one restart boundary on build-new.** Step 4 writes `.mcp.json` and halts the invocation — Claude Code only picks up new MCP env at session start, so the user restarts (or runs `/mcp` to reconnect) before Step 5 can run. Modify-existing has no restart boundary — its `.mcp.json` is already in place. @@ -78,6 +79,43 @@ Anything from a one-sentence domain description up to a fully-modeled workspace: | "Also track X in my event app" (existing workspace) | Run Intent → Plan (delta — only the new resources for X) → Import (additive merge) → Framework routing to the extend sub-skill. Skip Deployment and MCP Config. | | Explicit framework naming ("build it in Angular", "add an Angular module for X") | Do not activate. The user has chosen the framework; `formio-angular` or `formio-angular-resources` will handle it directly. | +## Using Resources within Forms — the right flow (and the anti-pattern to avoid) + +This is the highest-leverage thing to get right when an app has both a data model and bespoke forms. Hold it firmly and pass it to the planner. + +### The anti-pattern: establishing a Resource record inside a Form submission + +A common Form.io mistake is trying to solve two problems in one submission — **create the data-model record AND collect bespoke responses at the same moment**. The Job Application is the classic trap: a single "Job Application" form that both creates the `Applicant` record and captures the application answers, often by embedding the `Applicant` resource as a nested form so it is created inline. + +Why this is wrong: + +- The applicant's very first interaction should NOT be the Job Application form. Form.io forms are meant to be embedded inside an **application flow**, not to bootstrap a person's existence in the system. +- It conflates two separate concerns — *managing the Applicant record* and *collecting one application* — into a single brittle submission. +- It produces duplicate / throwaway Applicant records (one per application), defeating the whole point of a reusable data model, and it makes owner-based access and reporting messy. + +**Do NOT use a nested `form` component to create a Resource record from inside a bespoke Form.** Nested-form-for-creation is the mechanism that enables this anti-pattern. + +### The right flow: establish the Resource first, then reference it from the Form + +Separate the two concerns into two steps of the flow: + +1. **Establish the Resource record first**, as its own application concern — an onboarding / registration / profile step (e.g., the applicant onboards and an `Applicant` record is created). This is normal CRUD against the resource, managed by its own screens. +2. **Then the user fills the bespoke Form**, which *references* the already-established record rather than creating it. Two ways to wire the reference: + - **Disabled, pre-selected Select** — a `select` (dataSrc=resource) pointing at the resource, defaulted to the current user's record and set `disabled: true` so they cannot change it. The application is unambiguously linked to the right Applicant, and the user can't mis-select. + - **Submission `owner`** — when the relationship is 1:1 with the authenticated user (the user IS the subject), rely on the submission's `owner` and owner-based access instead of an explicit reference field. No select needed. + +Job Application, done right: the applicant onboards once (Applicant record created) → later opens the `JobApplication` form → the form shows their Applicant locked in a disabled Select (or simply owns the submission) plus the bespoke questions ("Why should we hire you?", "Earliest start date") → one clean submission = one application, linked to the existing Applicant. + +### What to tell the planner + +When the user's request implies a bespoke form over a data-model record, instruct the planner to: + +- Model the data-model record as a **Resource** managed by its **own** flow (onboarding / profile / admin CRUD). +- Model the bespoke collection as a separate **Form** that **references** the established Resource via a disabled, pre-selected Select OR via the submission `owner` — never via a nested form that creates the record. +- Never attach a Save action that creates the referenced Resource from the bespoke Form. + +See `formio-resource-planner` → "Resources vs. Forms — the core modeling decision" for the field-level shapes the planner emits. + ## The six steps ### Step 1 — Intent @@ -91,7 +129,7 @@ Determine whether this is a new app to build or an existing app to extend. See [ Invoke `formio-resource-planner` with the user's plain-language description. The planner runs its own two-phase approval gate (Phase A: Resource Map for review; Phase B: the paired artifacts `template.md` + `template.json` on approval). Do not add a second gate on top. -- **Build-new** → the planner produces a full-project pair: `template.md` (architectural intent, Access Matrix, ER + Access Flow diagrams) and `template.json` (every resource, role, form, and action the new app needs). +- **Build-new** → the planner produces a full-project pair: `template.md` (architectural intent, Access Matrix, ER + Access Flow diagrams) and `template.json` (every resource, role, form, and action the new app needs). The planner classifies each entity as a Resource (reusable data model) or a bespoke Form (purpose-specific data collection, e.g. a job application that references an already-established Applicant resource plus survey fields) — both land in the template. See "Using Resources within Forms" above: a Form references an established Resource (disabled Select or `owner`), it never creates the Resource inline. - **Modify-existing** → the planner produces a delta pair: `template.md` + `template.json` that contain ONLY the new resources, fields, or actions for the requested feature. The planner is told (a) that the project already exists and has resources loaded, (b) to plan only what is new, and (c) that the template will be merged additively on top of the existing project. The planner writes both files to the working directory as a paired set (same basename; same collision timestamp if either name is taken). Stash BOTH paths — you will pass them both to Step 5 (Import — reads `template.json`) and Step 6 (Framework routing — hands both to the framework skill). On the modify-existing branch, additionally stash a list of the delta resource names for the framework's extend sub-skill in Step 6. diff --git a/plugin/skills/formio-resource-planner/SKILL.md b/plugin/skills/formio-resource-planner/SKILL.md index d03d939..51a0dd6 100644 --- a/plugin/skills/formio-resource-planner/SKILL.md +++ b/plugin/skills/formio-resource-planner/SKILL.md @@ -16,14 +16,80 @@ You are a thinking partner that plans before it builds. Two distinct phases with - **Batch your questions.** When multiple related questions come up (e.g., all relationship cardinalities), ask them together in one `AskUserQuestion` call. Peppering the user one question at a time burns trust. - **Visualize twice, in two formats.** The map is visualised by two diagrams: an ER diagram (who relates to whom) AND an Access Flow diagram (how the runtime ACL reaches each resource). Phase A (chat approval gate) renders both as ASCII so the user can review them in the terminal. Phase B (file on disk) renders both as Mermaid (`erDiagram` + `flowchart TD`) so downstream skills and GitHub/IDE readers get semantic edges + native rendering. Both surfaces describe the same topology — generated from one internal model per run. - **Ground in Form.io primitives.** Every output claim must map to a real Form.io construct: resource, form, component, action, role, field-based access, group permission. +- **Pick the right kind per entity.** Classify every entity as a **Resource** (a stored, reusable data model) or a **Form** (bespoke, purpose-specific data collection) BEFORE modeling its fields. Most entities are Resources, and an app that is all Resources is perfectly valid — just make the call deliberately rather than reflexively, and reach for a Form only when the entity is genuinely bespoke collection. See ["Resources vs. Forms"](#resources-vs-forms--the-core-modeling-decision) below. - **Gate on approval.** Phase A (Resource Map) is for review. Do not emit `template.json` / `template.md` until the user has explicitly approved the map. See "The approval gate" below. - **Phase B is a pair.** Every Phase B emission writes `template.md` AND `template.json` together — same basename, same timestamp on collision. `template.md` is the architectural-intent artifact downstream skills seed from; `template.json` is its structured companion. Never emit one without the other. - **Actions are mandatory, not optional.** Every resource and form in `template.json` MUST have a corresponding entry in the top-level `actions` map — at minimum a Save Submission action so that submissions persist. Additional actions attach per semantics: Login on login forms, Role Assignment on register forms, Group Assignment on join resources. A `template.json` whose `actions` map lacks a `:save` entry for any resource is **broken by definition** — the resource will accept submissions in the UI but never store them. Treat a missing action as a hard failure and regenerate. See "Actions emission — required per resource" near Phase B, and [`references/template-json.md`](references/template-json.md) for exact shapes. - **Don't call the MCP server.** The skill produces plans plus the `template.md` / `template.json` artifact pair. It does not call `form_create` or any other MCP tool — importing is a separate, explicit user action. +## Resources vs. Forms — the core modeling decision + +Form.io has two kinds of entries — Resources and Forms. Decide the kind for every entity BEFORE modeling its fields. Resources are the right answer for most entities (and a perfectly valid app can be 100% Resources); the goal here is to recognize the cases that genuinely call for a Form, not to push entities into Forms unnecessarily. + +### Resource — the data model (a "noun") + +A **Resource** defines, displays, and stores a structured record. Resources are the nouns of the application — `Contact`, `Product`, `Project`, `Task`, `Applicant`. Each Resource auto-generates a REST API (`GET/POST/PUT/DELETE ${FORMIO_PROJECT_URL}/`), so the set of Resources is effectively the application's database — a structured, queryable backend much like Firebase. Reach for a Resource when: + +- The data is a persistent record other parts of the app read, reference, or report on. +- Other entities point at it (it is the target of a reference `select` — a foreign key). +- It needs its own CRUD management screens (list / create / edit / delete). +- The same shape recurs (every Contact has the same fields). + +A Resource can also be **embedded as a form** in the UI when the app needs CRUD management of that data model — the generated list / create / edit screens are simply how users maintain the resource's rows. "Embedded as a form" here is a UI concern, not a reason to model the data as `type: form`. + +### Form — bespoke data collection (a specific "ask") + +A **Form** (`type: form`) collects data for one specific purpose. Forms are the interactions — a job application, an onboarding survey, a support request, an event RSVP, a contact-us page. A Form's submission is the captured response, not a reusable record other parts of the data model depend on. Reach for a Form when: + +- The fields are specific to this one interaction and would not make sense as columns in a database table ("Why should we hire you?", "Rate your experience 1–5", "Any accessibility needs?"). +- It is survey-like, one-off, or workflow-driven rather than a record other things reference. +- It references a structured record (a Resource established earlier) AND adds extra, context-specific questions on top. + +A Form is not a second-class Resource. It frequently USES Resources two ways: + +1. **To populate choices** — a `select` with `dataSrc: resource` fills a dropdown / typeahead from a Resource (pick a Product, pick an Applicant). +2. **To reference an already-established record** — the Form points at a Resource record that was created earlier in the application flow (onboarding / profile / CRUD), then adds its own bespoke fields. Wire the reference one of two ways: a **disabled, pre-selected Select** (dataSrc=resource, defaulted to the user's record, `disabled: true`) or the submission **`owner`** (when the record is 1:1 with the authenticated user). See `references/template-json.md` → "select — reference an established Resource". + +> **Anti-pattern — do NOT create the Resource from inside the Form.** Never embed a Resource via a nested `form` component to create it inline, and never give a bespoke Form a Save action that creates the referenced Resource. Establishing the record and collecting the bespoke response are two separate flow steps. The data model is established first (its own onboarding / CRUD concern); the Form references it. See `formio-application` → "Using Resources within Forms — the right flow (and the anti-pattern to avoid)". + +### The litmus test + +Ask: *"Is this a record the app stores and reuses, or a response to a specific ask?"* + +- A record the app stores and reuses → **Resource**. +- A response to a specific ask, possibly wrapping a record → **Form**. + +When bespoke, survey-like fields are mixed with structured record fields, that is the tell for a **Form that references a Resource** — NOT a Resource with survey fields bolted on. Survey fields do not belong in the canonical data model; they are a supplemental, per-interaction extension of a base record that already exists. + +### Worked example — Job Application + +A recruiting app needs to capture job applications. + +- **`Applicant` (Resource)** — the reusable person record: `firstName`, `lastName`, `email`, `phone`, `resume` (file). A noun the app stores, lists, and references from other resources (`Interview`, `Offer`). It is established FIRST, by its own flow (the applicant onboards / creates a profile), and gets CRUD screens. +- **`JobApplication` (Form)** — the bespoke intake the applicant fills AFTER onboarding. It **references** the already-established `Applicant` record — via a disabled, pre-selected `applicant` Select (locked to their own record) or via the submission `owner` — and adds interaction-specific questions: "Why should we hire you?", "Earliest start date", "Salary expectation", "How did you hear about us?". These answers are meaningful only for this one application — they are not columns on the canonical `Applicant` record, so they live on the Form's submission, not the Resource. + +Two mistakes this guidance prevents: + +1. **Modeling both as resources** — pollutes the `Applicant` data model with one-off survey fields and loses the distinction between "a person we track" and "one application they submitted." +2. **Creating the `Applicant` from inside the `JobApplication` form** (e.g., a nested form, or a Save action that writes a new Applicant) — the anti-pattern. The form's first job becomes bootstrapping a person rather than collecting an application, producing duplicate Applicant records and breaking owner-based access. Establish the Applicant first; reference it from the form. See `formio-application` → "Using Resources within Forms". + +### Quick classification table + +| Entity / intent | Kind | Why | +| ------------------------------------------------ | -------- | --------------------------------------------------------------- | +| Contact, Company, Product, Project, Task, User | Resource | Persistent nouns; referenced by others; need CRUD | +| Order with line items | Resource | Stored record other things reference (invoices, reports) | +| Contact-us / feedback / support request | Form | One-off interaction; not referenced; survey-like | +| Event RSVP / registration | Form | Captures a response; may reference an established Attendee resource + bespoke Q's | +| Onboarding / intake questionnaire | Form | Survey-like; supplemental to a base record | +| Job application (over an Applicant resource) | Form | References an established Applicant (disabled Select / owner) + bespoke survey fields | +| Customer satisfaction survey | Form | Pure bespoke collection; no reusable record | + +When in doubt during the interview, ASK — present the entity and the two readings ("a record you manage" vs "a form people fill out") and let the user decide. Do not silently default to Resource. + ## The interview -Work through these five rounds. Compress or expand as the user's description warrants — if they named every entity and relationship explicitly, skip ahead; if they said only a brief phrase like "I want a CRM" or "build me a booking app," start from zero. +Work through these six rounds. Compress or expand as the user's description warrants — if they named every entity and relationship explicitly, skip ahead; if they said only a brief phrase like "I want a CRM" or "build me a booking app," start from zero. ### 1. Extract the named entities @@ -31,11 +97,17 @@ Re-read the user's prompt and list the nouns that sound like resources. For "Tas Confirm the list with the user in one question. They may add or drop entities. -### 2. Determine the relationships +### 2. Classify each entity — Resource or Form + +For every candidate from round 1, decide whether it is a **Resource** (a stored, reusable data model) or a **Form** (bespoke, purpose-specific data collection). Apply the litmus test in ["Resources vs. Forms"](#resources-vs-forms--the-core-modeling-decision) above. Make the call deliberately per entity: most will be Resources (an all-Resource app is fine), so reach for a Form only when an entity is genuinely bespoke collection — don't force one either way. + +Batch this with round 1's confirmation when you can: present the entity list already tagged `(Resource)` / `(Form)` and ask the user to correct any you misjudged. Explicitly call out any entity that looks like a bespoke Form referencing a Resource (e.g., a job application over an `Applicant` record, an RSVP over an `Attendee` record) so the user confirms the split between the reusable record (established first) and the per-interaction survey fields. + +### 3. Determine the relationships For every meaningful pair of entities, pin down the cardinality: 1:1, 1:N, or N:N. Ask as a batch. Draw a small ASCII sketch after the user answers. -### 3. Determine the user / auth model +### 4. Determine the user / auth model Ask (together): @@ -45,7 +117,7 @@ Ask (together): If the user has no authentication, say so explicitly and skip access rules — not every app needs login. -### 4. Determine the access / permission model +### 5. Determine the access / permission model Ask (together): @@ -55,7 +127,7 @@ Ask (together): - **Tenant-level** (strict multi-tenant isolation)? - Or some combination — e.g., "admins see everything, members see only their group's data." -### 5. Produce the resource map, then gate on approval +### 6. Produce the resource map, then gate on approval Emit the Phase A Resource Map (see "Phase A — Resource Map for review" below) as a single artifact. Then stop and ask the user to approve or revise. Only after approval, produce the Phase B `template.json`. @@ -142,6 +214,7 @@ When the user describes access ("reps only see their company's deals"), they alm | Multiple references (1:N embedded) | `select` with `multiple: true` and `data.resource` | | | File attachment | `file` | Requires a storage provider | | Email (for user login) | `email` | Always on the `user` resource | +| Reference an established Resource record from a Form | `select` (dataSrc=resource), often `disabled: true` + pre-selected | Links the form to a record created earlier in the flow; do NOT use a nested `form` to create the record inline (anti-pattern) | Full component reference: see the `formio-form` skill when you need exact JSON shapes. This cheat sheet is for planning, not generation. @@ -184,6 +257,20 @@ When the interview has enough signal, emit the resource map as a single fenced m - Group Assignment: group=, user= ← only when the join governs user access ... +## Forms + +(Include this section ONLY when the app has bespoke data-collection forms — job applications, surveys, RSVPs, intake/feedback forms. Omit the whole section for pure data-model apps. Auth forms — login/register — are described under "Users & Auth", NOT here.) + +- (type: form) + Purpose: <1 sentence — the specific interaction this form captures> + References: + Fields: + - : + - ... + Access: + Actions: + - : ← Save to its OWN submission only; never a Save that creates the referenced Resource + ## Users & Auth - User resource: `> @@ -241,7 +328,7 @@ If the user says "revise" or flags specific issues: update the map, re-show it, Only when the user has approved the map, emit the artifact PAIR — always both, always together: -1. **`template.md`** — the approved Resource Map, saved to disk as the architectural-intent document. Same structure as the Phase A map (Resources, Users & Auth, Roles, Access Matrix, ER Diagram, Access Flow Diagram, Companion artifact). See [`references/template-md.md`](references/template-md.md) for the complete spec. +1. **`template.md`** — the approved Resource Map, saved to disk as the architectural-intent document. Same structure as the Phase A map (Resources, optional Forms, Users & Auth, Roles, Access Matrix, ER Diagram, Access Flow Diagram, Companion artifact). See [`references/template-md.md`](references/template-md.md) for the complete spec. 2. **`template.json`** — the Form.io project-export JSON. Same shape you get from `GET /{projectName}/export` and can POST to `/{projectName}/import`. Each file is emitted in TWO forms at the same time: @@ -258,7 +345,7 @@ Each file is emitted in TWO forms at the same time: ### Transcript requirements -The markdown block MUST follow the section order in [`references/template-md.md`](references/template-md.md) exactly: `# Resource Map — ` → `## Resources` → `## Users & Auth` → `## Roles` → `## Access Matrix` → `## ER Diagram` → `## Access Flow Diagram` → `## Companion artifact`. Downstream graders and consumer skills key on these exact headings. +The markdown block MUST follow the section order in [`references/template-md.md`](references/template-md.md) exactly: `# Resource Map — ` → `## Resources` → (optional `## Forms`, only when the app has bespoke data-collection forms) → `## Users & Auth` → `## Roles` → `## Access Matrix` → `## ER Diagram` → `## Access Flow Diagram` → `## Companion artifact`. Downstream graders and consumer skills key on these exact headings. The `## ER Diagram` and `## Access Flow Diagram` sections in `template.md` MUST contain **Mermaid** fenced blocks (` ```mermaid\nerDiagram\n...``` ` and ` ```mermaid\nflowchart TD\n...``` ` respectively) — not ASCII. ASCII is for Phase A's chat-approval gate only. Every resource and join named in `## Resources` must appear as a node in both Mermaid blocks. See [`references/template-md.md`](references/template-md.md) → "ER Diagram section" and "Access Flow Diagram section" for the exact shapes, cardinality vocabulary, and worked examples for the three canonical patterns (owner-only, direct-child group, transitive group). @@ -437,6 +524,8 @@ Use these as structural references when deciding how to shape a new app's output ## Interview heuristics +- **When an entity carries survey-like or one-off fields** ("why should we hire you?", "rate 1–5", "any special requests?"), it is a **Form**, not a Resource — usually a Form that *references* an established Resource (disabled Select or `owner`) plus those bespoke fields. Do not bolt survey fields onto a data-model Resource, and do not create that Resource from inside the Form. See ["Resources vs. Forms"](#resources-vs-forms--the-core-modeling-decision). +- **When the user describes a workflow/interaction rather than a thing** ("people apply for a job", "guests RSVP", "customers file a complaint"), reach for a Form. When they describe a thing the app stores and reuses ("we track applicants", "we keep a product catalog"), reach for a Resource. A single feature often needs BOTH (an `Applicant` Resource and a `JobApplication` Form). - **When the user names 3+ entities and never mentions users**, stop and ask whether the app has authenticated users. Don't assume. - **When a relationship could be 1:N or N:N**, prefer N:N with a join resource if the relationship carries data (assigned-at, role-within-group, etc.). A join is cheap and backward-compatible; an embedded 1:N is not. - **When the user says "only see their team's / project's / tenant's data"**, that's group-based access. Reach for the join + Group Permissions pattern automatically and confirm. diff --git a/plugin/skills/formio-resource-planner/references/template-json.md b/plugin/skills/formio-resource-planner/references/template-json.md index a5519e2..bc8e1b5 100644 --- a/plugin/skills/formio-resource-planner/references/template-json.md +++ b/plugin/skills/formio-resource-planner/references/template-json.md @@ -84,8 +84,10 @@ Notes: `resources` and `forms` are parallel maps; both entries use the same schema except for `type`: -- `type: "resource"` for data resources (Project, Task, User, CompanyUser, etc.) -- `type: "form"` for user-facing forms that are not persistent data resources (login forms, public submission forms, signup forms) +- `type: "resource"` — a data model / "noun" the app stores, references, and exposes as a REST API (Project, Task, User, CompanyUser, Applicant, etc.). +- `type: "form"` — bespoke, purpose-specific data collection (job applications, surveys, RSVPs, feedback / contact forms, and the auth forms login/register). A Form captures a response to one interaction rather than a reusable record. It may reference Resources to populate `select` choices, and it may **reference an already-established Resource record** via a (often disabled, pre-selected) `select` or via the submission `owner`. A Form must NEVER create the referenced Resource on submit — see the anti-pattern callout below. + +See `formio-resource-planner/SKILL.md` → "Resources vs. Forms — the core modeling decision" for which entities should be modeled as resources vs forms. Most entities are resources, and an all-resource project is valid; use `type: "form"` for the genuinely bespoke cases (survey-like, one-off intakes) rather than forcing every entity one way or the other. ```jsonc "": { @@ -392,6 +394,65 @@ Because the parent's select uses `reference: true`, Form.io resolves the parent Add `"multiple": true` to the resource-select above. Use when the relationship carries no data of its own; otherwise model it as a join resource. +### select — reference an established Resource (pre-selected + disabled) + +Used inside a `type: "form"` entry to link the form to a Resource record that was **already created earlier in the application flow** (onboarding / profile / CRUD). The user does not create the record here — they fill the bespoke fields, and this select records which existing Resource the submission is about. Pre-select it to the current user's record and `disabled: true` so it cannot be changed. + +```jsonc +{ + "type": "select", + "key": "applicant", + "label": "Applicant", + "widget": "choicesjs", + "dataSrc": "resource", + "data": { "resource": "applicant" }, // machineName of the existing Resource + "template": "{{ item.data.firstName }} {{ item.data.lastName }}", + "reference": true, // store a resolvable reference, not a bare _id + "disabled": true, // locked — the user cannot change it + "defaultValue": "", // pre-populated at runtime to the user's own Applicant record + "validate": { "required": true }, + "input": true, + "tableView": true +} +``` + +The pre-selection (binding the select to the logged-in user's Applicant record) is wired in the UI layer by the framework skill, not in the template — the template's job is to declare the reference select, mark it `disabled`, and require it. When the relationship is strictly 1:1 with the authenticated user, you can omit this select entirely and rely on the submission **`owner`** instead (owner-based `submissionAccess`). + +> **Anti-pattern — never create the Resource from the bespoke Form.** Do NOT add a nested `form` component that creates the Resource inline, and do NOT give the Form a Save action whose `settings.resource` writes a new record into the referenced Resource. Creating the data-model record and collecting the bespoke response are two separate flow steps: the record is established first by its own flow; the Form only references it. See `formio-application` → "Using Resources within Forms — the right flow (and the anti-pattern to avoid)". + +### Form referencing an established Resource — full shape + +A bespoke `type: "form"` entry that references the already-created `applicant` Resource (via a disabled, pre-selected select) and adds interaction-specific questions. The bespoke fields live only on this form's submission; the `applicant` record is untouched. + +```jsonc +"jobApplication": { + "title": "Job Application", + "type": "form", + "name": "jobApplication", + "path": "job-application", + "tags": [], + "components": [ + { "type": "select", "key": "applicant", "label": "Applicant", "widget": "choicesjs", "dataSrc": "resource", "data": { "resource": "applicant" }, "template": "{{ item.data.firstName }} {{ item.data.lastName }}", "reference": true, "disabled": true, "validate": { "required": true }, "input": true, "tableView": true }, + { "type": "textarea", "key": "whyHire", "label": "Why should we hire you?", "input": true, "tableView": false }, + { "type": "datetime", "key": "earliestStart", "label": "Earliest start date", "enableTime": false, "input": true, "tableView": true }, + { "type": "number", "key": "salaryExpectation", "label": "Salary expectation", "input": true, "tableView": true }, + { "type": "button", "key": "submit", "label": "Submit", "action": "submit", "theme": "primary", "disableOnInvalid": true, "input": true } + ], + "access": [ + { "type": "read_all", "roles": ["administrator", "anonymous", "authenticated"] } + ], + "submissionAccess": [ + { "type": "create_own", "roles": ["authenticated"] }, + { "type": "read_own", "roles": ["authenticated"] }, + { "type": "read_all", "roles": ["administrator"] }, + { "type": "update_all", "roles": ["administrator"] }, + { "type": "delete_all", "roles": ["administrator"] } + ] +} +``` + +This entry needs a `jobApplication:save` action in `actions` like any other form — a plain Save that writes to its OWN submission (no `settings.resource` pointing at `applicant`). The `applicant` Resource keeps its own `applicant:save` action and is created by its own onboarding flow, long before this form is opened. The bespoke fields (`whyHire`, `earliestStart`, `salaryExpectation`) are intentionally NOT on the `applicant` Resource — they belong to this one interaction. + ### submit button ```jsonc diff --git a/plugin/skills/formio-resource-planner/references/template-md.md b/plugin/skills/formio-resource-planner/references/template-md.md index 41f9f3a..3b5b2a1 100644 --- a/plugin/skills/formio-resource-planner/references/template-md.md +++ b/plugin/skills/formio-resource-planner/references/template-md.md @@ -12,7 +12,7 @@ Treat the two files as a pair: same basename, same timestamp on collision (`temp ## Required section order -Sections appear in this exact order. Skills and graders rely on section headings — do not rename them. Every heading listed here is mandatory; omit the body only if the section genuinely has nothing (e.g., `Users & Auth` when the app is anonymous, then write `- None. App is public.` underneath). +Sections appear in this exact order. Skills and graders rely on section headings — do not rename them. Every heading listed here is mandatory EXCEPT `## Forms`, which is **conditional**: include it (immediately after `## Resources`) only when the app has bespoke data-collection forms, and omit the whole heading otherwise. For the remaining mandatory headings, omit only the body when the section genuinely has nothing (e.g., `Users & Auth` when the app is anonymous, then write `- None. App is public.` underneath). ```markdown # Resource Map — @@ -21,6 +21,8 @@ Sections appear in this exact order. Skills and graders rely on section headings ## Resources +## Forms + ## Users & Auth ## Roles @@ -65,6 +67,28 @@ For transitive group access, call out the hidden mirror on every grandchild: **invisible mirror that propagates group access from Account's team** ``` +## Forms section (conditional) + +Include this section ONLY when the app has bespoke, purpose-specific data-collection forms — job applications, surveys, RSVPs, intake/feedback forms. A **Form** (`type: form`) captures a response for one interaction rather than a reusable record; see `SKILL.md` → "Resources vs. Forms — the core modeling decision" for the classification rule. Omit the entire `## Forms` heading for pure data-model apps. + +Auth forms (login / register) are NOT listed here — they belong under `## Users & Auth`. This section is only for application-level bespoke forms. + +One block per form. Call out which Resource (if any) the form **references** (a record established earlier in the flow) and how. A bespoke form references an existing record — it never creates that record on submit; see `SKILL.md` → "Resources vs. Forms" and `formio-application` → "Using Resources within Forms" for the anti-pattern. + +```markdown +- (type: form) + Purpose: <1 sentence — the specific interaction this form captures> + References: + Fields: + - : + - ... + Access: + Actions: + - : ← Save to its OWN submission only; never a Save that creates the referenced Resource +``` + +Forms may also appear in the ER Diagram (wired to any Resource they reference) and in the Access Flow Diagram when their submission access is non-trivial, but this is optional — the grader does not require forms to appear in either diagram. + ## Users & Auth section Bulleted facts only. Keep it parseable.