diff --git a/scripts/build-mcp-openapi.mjs b/scripts/build-mcp-openapi.mjs index 8b1c2c9..c4048ba 100644 --- a/scripts/build-mcp-openapi.mjs +++ b/scripts/build-mcp-openapi.mjs @@ -534,18 +534,35 @@ const coreMcpOperations = { } }, formsRowsList: { - summary: "List submissions or rows for a form", + summary: "List form submissions", description: - "Lists submissions, also called rows or records, for a specific form in the active workspace. This may expose customer-submitted data.", + "Lists form submissions (rows) with dashboard-parity filters: page/page_size, search, sort_by, status, created_by/updated_by/tags, submit_time/created_at/updated_at ranges (_gte/_lte), and dynamic {fieldSlug}/{fieldSlug}_{operator} field filters. RowQueryUtils supports contains/has/equal/exact/lt/lte/gt/gte and not_ variants; comma-separated bare field values become list filters. Response includes rows, count, and often top_fields for table columns.", parameterDescriptions: { - slug: "Form slug.", + slug: "Form slug whose submissions to list.", + page: "1-based page number.", + page_size: "Submissions per page.", + search: "Case-insensitive search across submission values.", + sort_by: "Comma-separated sort fields; prefix with - for descending.", + status: "Row status filter; use all (or omit) for every status.", + created_by: "Filter by creator first name or email.", + updated_by: "Filter by last updater first name or email.", + tags: "Comma-separated tag slugs.", submit_number: "Submission number filter.", - tracking_code: "Submission tracking code filter." + tracking_code: "Submission tracking code filter.", + created_at: "Created-at date filter (YYYY-MM-DD); also created_at_gte/lte.", + updated_at: "Updated-at date filter (YYYY-MM-DD); also updated_at_gte/lte.", + submit_time: "Submit-time timestamp filter; also submit_time_gte/lte.", + created_at_gte: "Created-at lower bound (inclusive).", + created_at_lte: "Created-at upper bound (inclusive).", + updated_at_gte: "Updated-at lower bound (inclusive).", + updated_at_lte: "Updated-at upper bound (inclusive).", + submit_time_gte: "Submit-time lower bound (inclusive).", + submit_time_lte: "Submit-time upper bound (inclusive)." }, mcp: { - tool_name: "list_form_rows", - aliases: ["list_submissions", "show_submissions", "list_form_records"], - intent: "List submitted rows for a Formaloo form.", + tool_name: "list_form_submissions", + aliases: ["list_form_rows", "list_rows", "list_submissions", "show_submissions", "list_form_records"], + intent: "List submitted rows/submissions for a Formaloo form with filters.", requires_workspace: true, read_only: true, destructive: false, diff --git a/scripts/normalize-openapi.mjs b/scripts/normalize-openapi.mjs index 2bf15b4..d48187c 100644 --- a/scripts/normalize-openapi.mjs +++ b/scripts/normalize-openapi.mjs @@ -1973,6 +1973,216 @@ function enrichRowSchemas() { } } +function upsertQueryParameter(operation, parameter) { + if (!operation || !parameter?.name) { + return; + } + operation.parameters = Array.isArray(operation.parameters) ? operation.parameters : []; + const index = operation.parameters.findIndex( + (item) => item?.in === "query" && item?.name === parameter.name + ); + if (index >= 0) { + operation.parameters[index] = { + ...operation.parameters[index], + ...parameter, + schema: { + ...(operation.parameters[index].schema || {}), + ...(parameter.schema || {}) + }, + description: parameter.description || operation.parameters[index].description + }; + return; + } + operation.parameters.push(parameter); +} + +function enrichFormsRowsListOperation() { + const listRows = spec.paths["/v3.0/forms/{slug}/rows/"]?.get; + if (!listRows) { + return; + } + + listRows.summary = listRows.summary || "List form submissions"; + const existingDescription = String(listRows.description || "").trim(); + const filterNotice = + "Supports dashboard-parity filters: pagination (`page`, `page_size`), `search`, `sort_by`, `status`, meta (`created_by`, `updated_by`, `tags`, `tracking_code`, `submit_number`), timestamp/date ranges (`submit_time`/`created_at`/`updated_at` plus `_gte`/`_lte`/`_gt`/`_lt`), and dynamic `{fieldSlug}` / `{fieldSlug}_{operator}` field filters. `RowQueryUtils` supports contains/has/equal/exact/lt/lte/gt/gte and `not_` variants; comma-separated bare field values become list filters. Response includes `rows`, `count`, and often `top_fields` for table columns."; + if (!existingDescription.includes("dashboard-parity filters")) { + listRows.description = existingDescription + ? `${existingDescription}\n\n${filterNotice}` + : filterNotice; + } + + const queryParams = [ + { + in: "query", + name: "page", + required: false, + schema: { type: "integer" }, + description: "A page number within the paginated result set." + }, + { + in: "query", + name: "page_size", + required: false, + schema: { type: "integer" }, + description: "Number of results to return per page." + }, + { + in: "query", + name: "pagination", + required: false, + schema: { type: "string" }, + description: "Set to `0` to disable pagination for this list." + }, + { + in: "query", + name: "search", + required: false, + schema: { type: "string" }, + description: "Case-insensitive search across submission values." + }, + { + in: "query", + name: "sort_by", + required: false, + schema: { type: "string" }, + description: + "Comma-separated sort fields. Prefix with `-` for descending (for example `-submit_time`, `-created_at`, or a field slug)." + }, + { + in: "query", + name: "status", + required: false, + schema: { type: "string" }, + description: "Filter by row status. Use `all` (or omit) for every status." + }, + { + in: "query", + name: "tags", + required: false, + schema: { type: "string" }, + description: "Comma-separated list of tag slugs." + }, + { + in: "query", + name: "tracking_code", + required: false, + schema: { type: "string" }, + description: "Filter by tracking code." + }, + { + in: "query", + name: "submit_number", + required: false, + schema: { type: "string" }, + description: "Filter by submit number." + }, + { + in: "query", + name: "created_by", + required: false, + schema: { type: "string" }, + description: "Filter by creator first name or email (icontains)." + }, + { + in: "query", + name: "updated_by", + required: false, + schema: { type: "string" }, + description: "Filter by last updater first name or email (icontains)." + }, + { + in: "query", + name: "created_at", + required: false, + schema: { type: "string", format: "date" }, + description: + "Date filter for created_at (`YYYY-MM-DD`). Range variants: `created_at_gte`, `created_at_lte`, `created_at_gt`, `created_at_lt`." + }, + { + in: "query", + name: "created_at_gte", + required: false, + schema: { type: "string", format: "date-time" }, + description: "Created-at lower bound (inclusive)." + }, + { + in: "query", + name: "created_at_lte", + required: false, + schema: { type: "string", format: "date-time" }, + description: "Created-at upper bound (inclusive)." + }, + { + in: "query", + name: "updated_at", + required: false, + schema: { type: "string", format: "date" }, + description: + "Date filter for updated_at (`YYYY-MM-DD`). Range variants: `updated_at_gte`, `updated_at_lte`, `updated_at_gt`, `updated_at_lt`." + }, + { + in: "query", + name: "updated_at_gte", + required: false, + schema: { type: "string", format: "date-time" }, + description: "Updated-at lower bound (inclusive)." + }, + { + in: "query", + name: "updated_at_lte", + required: false, + schema: { type: "string", format: "date-time" }, + description: "Updated-at upper bound (inclusive)." + }, + { + in: "query", + name: "submit_time", + required: false, + schema: { type: "string", format: "date-time" }, + description: + "Timestamp filter for submit time (maps to row created_at). Also accepts `submit_time_gte`, `submit_time_lte`, `submit_time_gt`, `submit_time_lt`." + }, + { + in: "query", + name: "submit_time_gte", + required: false, + schema: { type: "string", format: "date-time" }, + description: "Submit-time lower bound (inclusive)." + }, + { + in: "query", + name: "submit_time_lte", + required: false, + schema: { type: "string", format: "date-time" }, + description: "Submit-time upper bound (inclusive)." + } + ]; + + for (const parameter of queryParams) { + upsertQueryParameter(listRows, parameter); + } + + const paginated = spec.components?.schemas?.PaginatedRowList; + if (paginated?.properties && !paginated.properties.top_fields) { + paginated.properties.top_fields = { + type: "array", + description: + "Optional column hints for table UIs. Typically form field references (slug/title/type). Table columns are the form’s fields; when present, prefer this order.", + items: { + type: "object", + additionalProperties: true, + properties: { + slug: { type: "string" }, + title: { type: "string" }, + type: { type: "string" }, + alias: { type: "string" } + } + } + }; + } +} + function enrichBlockSchemas() { spec.components.schemas.FormalooAccessSettings = { type: "object", @@ -2550,6 +2760,7 @@ enrichFormDisplaySubmitContract(); enrichBoardDeleteOperation(); enrichChoiceFieldSchemas(); enrichRowSchemas(); +enrichFormsRowsListOperation(); enrichBlockSchemas(); enrichBoardSchemas(); enrichFormSummarySchemas(); diff --git a/spec/docs/v3.0/boards/board_slug/blocks/block_slug/content/get.md b/spec/docs/v3.0/boards/board_slug/blocks/block_slug/content/get.md index 8681417..9ecaf01 100644 --- a/spec/docs/v3.0/boards/board_slug/blocks/block_slug/content/get.md +++ b/spec/docs/v3.0/boards/board_slug/blocks/block_slug/content/get.md @@ -8,7 +8,7 @@ If you want to search on the whole fields: {"fields_filters":{}, "search": "lorem"} ``` -- If you want to apply the search only on a given request, you can send it on the query params: `?seach=lorem` +- If you want to apply the search only on a given request, you can send it on the query params: `?search=lorem` Notes: - The search term is case-insensitive, so `Lorem` and `lorem` are the same. diff --git a/spec/docs/v3.0/forms/slug/rows/bulk-delete/post.md b/spec/docs/v3.0/forms/slug/rows/bulk-delete/post.md index 4ce108b..552eb31 100644 --- a/spec/docs/v3.0/forms/slug/rows/bulk-delete/post.md +++ b/spec/docs/v3.0/forms/slug/rows/bulk-delete/post.md @@ -12,4 +12,4 @@ Use this only when the user intends to remove multiple submissions from the form ## Behavior -This is a bulk delete via `POST` (not a `DELETE` verb). Request body includes `slugs_list`. Returns `200` with an empty `data` object on success. There is no separate confirmation step. +This is a bulk delete via `POST` (not a `DELETE` verb). Request body must include `slugs_list` (array of row/submission slugs). Returns success with an empty `data` object. The HTTP API does not add a confirmation flag; agent clients (Formaloo MCP) should require an explicit `confirm=true` before calling this operation. diff --git a/spec/docs/v3.0/forms/slug/rows/get.md b/spec/docs/v3.0/forms/slug/rows/get.md index e4450d2..f9421ad 100644 --- a/spec/docs/v3.0/forms/slug/rows/get.md +++ b/spec/docs/v3.0/forms/slug/rows/get.md @@ -1,4 +1,77 @@ -Lists submissions for a specific form in admin context. Use this for response inboxes, table views, search, filtering, sorting, and back-office review of collected data. +Lists submissions (rows) for a specific form in admin context. Use this for response inboxes, table views, search, filtering, sorting, and back-office review of collected data. + +Sources of truth for filter behavior: Formaloo dashboard row fetchers (`formsRowsList` query params) and `formz_core` `RowQueryUtils` / `FormRowsView` (`date_filter_fields`, `timestamp_filter_fields`, `filter_fields`, plus `status` in `get_extra_filters`). + +## Query parameters + +### Pagination and text search + +| Param | Description | +| --- | --- | +| `page` | 1-based page number | +| `page_size` | Results per page | +| `pagination` | Set to `0` to disable pagination | +| `search` | Case-insensitive search across submission values | +| `sort_by` | Comma-separated fields; prefix with `-` for descending (for example `-submit_time`, `-created_at`, or a field slug) | + +### Submission status and meta filters + +| Param | Description | +| --- | --- | +| `status` | Row status filter. Omit or use `all` for every status | +| `tags` | Comma-separated tag slugs | +| `tracking_code` | Exact tracking code | +| `submit_number` | Exact submit number | +| `created_by` | Match creator first name or email (icontains) | +| `updated_by` | Match last updater first name or email (icontains) | + +### Date and timestamp ranges + +Exact and range filters use the dashboard/`idealib` convention: + +- Date fields (`created_at`, `updated_at`): exact `YYYY-MM-DD` via the bare param +- Timestamp fields (`submit_time`, `created_at`, `updated_at`): bare value or `*_lt` / `*_lte` / `*_gt` / `*_gte` + +Examples: + +- `?submit_time_gte=2024-01-01T00:00:00Z&submit_time_lte=2024-12-31T23:59:59Z` +- `?created_at_gte=2024-01-01T00:00:00Z` +- `?updated_at=2024-08-07` (date-only exact day filter when sent as a date field) + +`submit_time` maps to the row `created_at` timestamp on the backend. + +### Dynamic field filters + +Any form field slug can be used as a query key. Operators follow `{fieldSlug}` or `{fieldSlug}_{operator}` (see `RowQueryUtils`): + +| Operator suffix | Meaning | +| --- | --- | +| _(none)_ / `exact` / `equal` | Exact match (text fields default to case-insensitive exact) | +| `icontains` / `contains` / `has` | Contains | +| `not_contains` / `not_has` | Does not contain | +| `not_equal` | Not equal | +| `gt` / `gte` / `lt` / `lte` | Comparisons (numbers/dates) | + +Comma-separated values on the bare field key become a list filter (for example `?city=NYC,LA`). There is no `_in` or `not_in` suffix in `RowQueryUtils`. + +Examples: + +- `?field_email=amir@example.com` +- `?field_email_icontains=example.com` +- `?satisfaction_gte=4` +- `?multi_select_has=choice_abc` +- `?city=NYC,LA` + +Unknown keys that do not match a form field slug (or meta/order patterns) are ignored by `RowQueryUtils`. + +## Response + +`data` includes: + +- `rows` — submission objects (answer keys are field slugs in `data` / `rendered_data`) +- `count` — total matching rows +- pagination fields (`next`, `previous`, `page_size`, `page_count`, `current_page`) when pagination is enabled +- `top_fields` — optional column hints for table UIs (form field references). Table columns are the form’s fields; when present, `top_fields` is the preferred column order for dashboards ## Example response (`200`) @@ -16,11 +89,19 @@ Lists submissions for a specific form in admin context. Use this for response in "page_size": 10, "page_count": 1, "current_page": 1, + "top_fields": [ + { "slug": "field_JgKgX2vVPh", "title": "Name", "type": "short_text" }, + { "slug": "field_GFUOFRUTeg", "title": "Rating", "type": "dropdown" } + ], "rows": [ { "slug": "row_a1b2c3d4", "created_at": "2024-08-07T14:20:11.000000Z", "updated_at": "2024-08-07T14:20:11.000000Z", + "data": { + "field_JgKgX2vVPh": "Jane Doe", + "field_GFUOFRUTeg": "choice_excellent" + }, "rendered_data": { "field_JgKgX2vVPh": "Jane Doe", "field_GFUOFRUTeg": "Excellent" @@ -30,6 +111,10 @@ Lists submissions for a specific form in admin context. Use this for response in "slug": "row_e5f6g7h8", "created_at": "2024-08-07T15:02:44.000000Z", "updated_at": "2024-08-07T15:02:44.000000Z", + "data": { + "field_JgKgX2vVPh": "John Smith", + "field_GFUOFRUTeg": "choice_good" + }, "rendered_data": { "field_JgKgX2vVPh": "John Smith", "field_GFUOFRUTeg": "Good" @@ -39,5 +124,3 @@ Lists submissions for a specific form in admin context. Use this for response in } } ``` - -Row answer keys are field slugs. Use search, pagination, and filter query parameters supported by the endpoint when building inbox or table views.