diff --git a/.claude/skills/plugin-api-docs/SKILL.md b/.claude/skills/plugin-api-docs/SKILL.md index 58828f2b08..c44cfd991d 100644 --- a/.claude/skills/plugin-api-docs/SKILL.md +++ b/.claude/skills/plugin-api-docs/SKILL.md @@ -20,9 +20,13 @@ content, and a checker. | `npm run plugin-api:generate` | Rewrites generated blocks in the docs, writes the request schema, reports drift | | `npm run plugin-api:lint` | Checks the docs against the code and the conventions | | `npm run plugin-api:check` | Both of the above in report-only mode — what CI runs | +| `npm run plugin-api:tester` | Writes the plugin API Tester's `reference-templates.json` from the request examples on the resource pages | +| `npm run plugin-api:tester:check` | Reports whether the tester's copy is stale, without writing | -`markers.mjs` and `inventory.mjs` are shared helpers, not entry points: the first defines a -well-formed generated-block marker, the second is the one way both scripts run the extractor. +`markers.mjs`, `inventory.mjs`, `coverage.mjs` and `markdown.mjs` are shared helpers, not entry +points: the first defines a well-formed generated-block marker, the second is the one way the +scripts run the extractor, the third says which page documents which resource, and the fourth +holds the fenced-code and heading-anchor rules. The generator consults `markers.mjs` *before* rewriting a page and skips any page whose markers it cannot prove safe; that file explains what a rewrite would otherwise destroy. @@ -42,6 +46,12 @@ baseline; either document it or add it to the baseline deliberately. **To see whether anything has drifted, without writing:** `npm run plugin-api:check`. +**After changing a request example on a resource page:** `npm run plugin-api:tester`, then commit +the regenerated `DataInteractiveAPITester/resources/reference-templates.json` in +`codap-data-interactives` (base `gh-pages`, which is live as soon as it merges). The tester +scripts assume that repo is cloned beside `codap`. CI cannot check this file, because it lives +in the other repo, so run `npm run plugin-api:tester:check` before a release. + ## What is generated and what is not Generated blocks are delimited in the markdown: diff --git a/v3/doc/plugin-api/resources/adornment.md b/v3/doc/plugin-api/resources/adornment.md index ffe66a2ec2..32cc972fd8 100644 --- a/v3/doc/plugin-api/resources/adornment.md +++ b/v3/doc/plugin-api/resources/adornment.md @@ -242,6 +242,17 @@ can display them. The adornment remains in the graph's store with `isVisible: false`, so a subsequent `get` succeeds rather than reporting it missing. +**Show it again.** Because the hidden adornment is still in the store, `update` can turn it back +on: + +```json +{ + "action": "update", + "resource": "component[Heights].adornment", + "values": { "type": "Mean", "isVisible": true } +} +``` + ## Notifications Toggling an adornment emits a component notification whose operation is specific to the diff --git a/v3/doc/plugin-api/resources/attribute.md b/v3/doc/plugin-api/resources/attribute.md index 2eaebffce6..df9bfa6cf5 100644 --- a/v3/doc/plugin-api/resources/attribute.md +++ b/v3/doc/plugin-api/resources/attribute.md @@ -252,6 +252,15 @@ Start a drag from inside a plugin, so the user can drop an attribute onto a grap } ``` +Delete an attribute, and with it its value in every case: + +```json +{ + "action": "delete", + "resource": "dataContext[Mammals].attribute[Notes]" +} +``` + ## Known limitations **`delete` sends no notification.** Other plugins are not told that the attribute is gone. v2 diff --git a/v3/doc/plugin-api/resources/component.md b/v3/doc/plugin-api/resources/component.md index da50018596..9e5000414d 100644 --- a/v3/doc/plugin-api/resources/component.md +++ b/v3/doc/plugin-api/resources/component.md @@ -511,6 +511,16 @@ Bring a component to the front, then rescale it: } ``` +Close a component. A case table, case card, calculator or guide is hidden rather than removed — +see [Known limitations](#known-limitations): + +```json +{ + "action": "delete", + "resource": "component[Height by Weight]" +} +``` + ## Known limitations **Creating or closing a component cannot be undone.** v2 added `create` and `delete` on diff --git a/v3/doc/plugin-api/resources/data-context.md b/v3/doc/plugin-api/resources/data-context.md index dd8640e562..f16201b410 100644 --- a/v3/doc/plugin-api/resources/data-context.md +++ b/v3/doc/plugin-api/resources/data-context.md @@ -131,6 +131,88 @@ Create a data context with one collection and two attributes: } ``` +Read it back, after one case has been added. The response describes the structure — collections +and attributes — and has no cases: + +```json +{ + "action": "get", + "resource": "dataContext[Mammals]" +} +``` + +```json +{ + "success": true, + "values": { + "type": "DG.DataContext", + "document": 1, + "guid": 41, + "id": 41, + "name": "Mammals", + "title": "Mammal measurements", + "collections": [ + { + "attrs": [ + { + "name": "Species", + "type": "categorical", + "title": "Species", + "cid": "ATTR43", + "_categoryMap": { + "lion": "#ff6800", + "__order": [ "lion" ], + "attribute-color": "#ff6800", + "low-attribute-color": "#f5e9e0", + "high-attribute-color": "#ff6800" + }, + "editable": true, + "hidden": false, + "renameable": true, + "deleteable": true, + "guid": 43, + "id": 43 + }, + { + "name": "Mass", + "type": "numeric", + "title": "Mass", + "cid": "ATTR44", + "_categoryMap": { + "190": "#ff6800", + "__order": [ "190" ], + "attribute-color": "#803e75", + "low-attribute-color": "#eee7ed", + "high-attribute-color": "#803e75" + }, + "editable": true, + "hidden": false, + "renameable": true, + "deleteable": true, + "guid": 44, + "id": 44, + "precision": 2 + } + ], + "guid": 42, + "id": 42, + "name": "Cases", + "title": "Cases", + "type": "DG.Collection" + } + ], + "flexibleGroupingChangeFlag": false, + "preventReorg": false, + "setAsideItems": [], + "contextStorage": { "_links_": { "selectedCases": [] } } + } +} +``` + +Keys whose value is `undefined`, such as each attribute's `formula` and the collection's `parent`, +are left out above; a plugin receives them, as described under +[What `get` returns](#what-get-returns). + Sort a data context by one attribute, descending: ```json @@ -159,6 +241,15 @@ Set two cases aside, then restore everything that is set aside: } ``` +Delete a data context, with all of its cases: + +```json +{ + "action": "delete", + "resource": "dataContext[Mammals]" +} +``` + ## Known limitations **`title` is `undefined` when the data context has no title of its own.** v2 reported the title diff --git a/v3/doc/plugin-api/resources/document.md b/v3/doc/plugin-api/resources/document.md index a07986bc0c..e47fae54af 100644 --- a/v3/doc/plugin-api/resources/document.md +++ b/v3/doc/plugin-api/resources/document.md @@ -166,6 +166,17 @@ sendRequest({ action: 'get', resource: 'document' }) sendRequest({ action: 'update', resource: 'document', values: savedState }) ``` +**Replace the document.** `values` is the whole v2 document, such as one saved from an earlier +`newDocumentState` notification: + +```json +{ + "action": "update", + "resource": "document", + "values": { "...": "a complete v2 document, as delivered in values.state" } +} +``` + ## Notifications | Resource | Operation | When | diff --git a/v3/package.json b/v3/package.json index 6b1c2e348e..af6156792b 100644 --- a/v3/package.json +++ b/v3/package.json @@ -69,6 +69,8 @@ "plugin-api:generate": "node scripts/plugin-api/generate-plugin-api-docs.mjs", "plugin-api:check": "node scripts/plugin-api/generate-plugin-api-docs.mjs --check; g=$?; node scripts/plugin-api/lint-plugin-api-docs.mjs; l=$?; exit $((g>l?g:l))", "plugin-api:lint": "node scripts/plugin-api/lint-plugin-api-docs.mjs", + "plugin-api:tester": "node scripts/plugin-api/generate-tester-templates.mjs --out ../../codap-data-interactives/DataInteractiveAPITester/resources/reference-templates.json", + "plugin-api:tester:check": "node scripts/plugin-api/generate-tester-templates.mjs --check --out ../../codap-data-interactives/DataInteractiveAPITester/resources/reference-templates.json", "lint:build": "eslint --cache --cache-strategy content --cache-location \".cache/eslint/build\" -c \"eslint.build.config.mjs\" \"./src/**/*.{js,jsx,mjs,ts,tsx}\" \"./cypress/**/*.{js,jsx,mjs,ts,tsx}\"", "lint:fix": "eslint --cache --fix \"./src/**/*.{js,jsx,ts,tsx}\" \"./cypress/**/*.{js,jsx,ts,tsx}\"", "lint:style": "eslint -c \".eslintrc.style.cjs\" \"./src/**/*.{js,jsx,ts,tsx}\" \"./cypress/**/*.{js,jsx,ts,tsx}\"", diff --git a/v3/scripts/plugin-api/generate-tester-templates.mjs b/v3/scripts/plugin-api/generate-tester-templates.mjs new file mode 100644 index 0000000000..f4945ed50e --- /dev/null +++ b/v3/scripts/plugin-api/generate-tester-templates.mjs @@ -0,0 +1,176 @@ +#!/usr/bin/env node +// +// generate-tester-templates.mjs +// +// Writes the request templates for the plugin API Tester (DataInteractiveAPITester in the +// codap-data-interactives repo) from the request examples in v3/doc/plugin-api/resources/. +// +// Every ```json block on a resource page whose `action` is one the inventory lists for its +// resource type becomes a template. That test is what separates a request a plugin sends from a +// notification CODAP sends, which the pages also show as json: `logMessageNotice` and +// `documentChangeNotice` are not resources, and `notify` on `global` is not an action a plugin +// can send. An array block becomes one template per element. +// +// Each template is named from the prose that introduces its example — the bold lead-in, or the +// first sentence of the paragraph before the block — and carries the URL of the section it came +// from. A lead that only makes sense beside its neighbors ("Read it.") gets the resource +// appended. An example that appears on more than one page is kept once, from its own resource's +// page where there is one. +// +// Templates with no counterpart in the reference live in the tester's own tests.json; this +// script does not touch that file. +// +// Usage: npm run plugin-api:tester (assumes codap-data-interactives is cloned beside codap) +// npm run plugin-api:tester:check (exit 1 if the tester's copy is stale) +// node generate-tester-templates.mjs --out [--check] +// +import { readFileSync, writeFileSync, existsSync } from "node:fs" +import { fileURLToPath } from "node:url" +import { dirname, join } from "node:path" +import { buildPageFor, listPages } from "./coverage.mjs" +import { readInventory } from "./inventory.mjs" +import { headingsOf, maskFences } from "./markdown.mjs" + +const here = dirname(fileURLToPath(import.meta.url)) +const resourcesDir = join(here, "..", "..", "doc", "plugin-api", "resources") +const sourceBaseUrl = + "https://github.com/concord-consortium/codap/blob/main/v3/doc/plugin-api/resources/" + +const usage = "usage: generate-tester-templates.mjs --out [--check]" +const args = process.argv.slice(2) +const outIndex = args.indexOf("--out") +const outPath = outIndex >= 0 ? args[outIndex + 1] : undefined +const check = args.includes("--check") +if (!outPath || outPath.startsWith("--")) { + console.error(usage) + process.exit(1) +} + +const inventory = readInventory() +const actionsByType = new Map(inventory.resources.map(r => [r.name, new Set(r.actions)])) +const pageFor = buildPageFor(resourcesDir, inventory.resources) + +// `dataContext[Mammals].collection[Cases].attribute[Height]` -> `attribute` +const resourceTypeOf = resource => resource.replace(/\[[^\]]*]/g, "").replace(/.*\./, "") + +const kMaxNameLength = 80 + +// The text a template is named from: the bold lead-in of the paragraph, or its first sentence. +// Returns undefined for a paragraph that is not prose — a heading, a list, a table, code. +function leadOf(paragraph) { + if (/^(#|- |\* |\d+\. |\||