From 80c101f5a60021456582b1d790739560e75fed9a Mon Sep 17 00:00:00 2001 From: "Roger Tuan (DatoCMS)" Date: Thu, 7 May 2026 17:10:24 -0700 Subject: [PATCH 1/9] Fix scroll-to-field min-fields setting silently reverting to default The plugin's `minFieldsToShow` config used a TextField that submitted the value as a string, which then failed `typeof === 'number'` in `isValidGlobalParams` and made `normalizeGlobalParams` reset to the default of 5 on the next read. Coerce the value to an integer at submit time, validate that it is at least 1, and render as a numeric input so the browser also clamps below-1 input. Co-Authored-By: Claude Opus 4.7 (1M context) --- field-anchor-menu/README.md | 6 +- field-anchor-menu/package-lock.json | 29 ++++---- field-anchor-menu/package.json | 2 +- .../src/entrypoints/ConfigScreen/index.tsx | 66 +++++++++++++++++-- 4 files changed, 85 insertions(+), 18 deletions(-) diff --git a/field-anchor-menu/README.md b/field-anchor-menu/README.md index dfd9f07d..401ce3f2 100644 --- a/field-anchor-menu/README.md +++ b/field-anchor-menu/README.md @@ -1,4 +1,8 @@ # Scroll to Field A simple plugin that displays a menu on the sidebar with anchor links -to all fields in your record. Click on the link to scroll to the selected field. \ No newline at end of file +to all fields in your record. Click on the link to scroll to the selected field. + +## Changelog + +- 0.1.14 - Fixed the "minimum number of fields" plugin setting silently reverting to the default of 5 every time. The config screen used a `TextField`, which submitted the value as a string; the runtime then failed `typeof value === 'number'` and `normalizeGlobalParams` reset it. The field now coerces the value to an integer on submit, validates that it is at least 1, and renders as a numeric input so the browser surfaces the constraint up front. \ No newline at end of file diff --git a/field-anchor-menu/package-lock.json b/field-anchor-menu/package-lock.json index a68db548..4c53cf62 100644 --- a/field-anchor-menu/package-lock.json +++ b/field-anchor-menu/package-lock.json @@ -1,12 +1,12 @@ { "name": "datocms-plugin-field-anchor-menu", - "version": "0.1.13", + "version": "0.1.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "datocms-plugin-field-anchor-menu", - "version": "0.1.13", + "version": "0.1.14", "dependencies": { "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -54,6 +54,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1413,6 +1414,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1522,6 +1524,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1601,6 +1604,15 @@ "node": ">=10" } }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -2177,6 +2189,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2229,6 +2242,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2238,6 +2252,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2569,6 +2584,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -2660,15 +2676,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" - }, - "node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } } } } diff --git a/field-anchor-menu/package.json b/field-anchor-menu/package.json index 4d74127d..079f2b62 100644 --- a/field-anchor-menu/package.json +++ b/field-anchor-menu/package.json @@ -1,6 +1,6 @@ { "name": "datocms-plugin-field-anchor-menu", - "version": "0.1.13", + "version": "0.1.14", "homepage": "https://github.com/datocms/plugins/tree/master/field-anchor-menu#readme", "description": "A plugin that displays an anchor links menu on the sidebar to scroll directly to a specific field", "keywords": [ diff --git a/field-anchor-menu/src/entrypoints/ConfigScreen/index.tsx b/field-anchor-menu/src/entrypoints/ConfigScreen/index.tsx index 537e8f7e..58da0c9f 100644 --- a/field-anchor-menu/src/entrypoints/ConfigScreen/index.tsx +++ b/field-anchor-menu/src/entrypoints/ConfigScreen/index.tsx @@ -17,27 +17,83 @@ type Props = { ctx: RenderConfigScreenCtx; }; +const MIN_FIELDS_FLOOR = 1; + +// `TextField` returns the raw string value from the input; coerce here so the +// stored params satisfy `isValidGlobalParams` (it checks for `number`, +// otherwise the value is silently reset to the default on the next read). +function parseMinFieldsToShow(value: unknown): number { + if (typeof value === 'number' && Number.isFinite(value)) { + return Math.max(MIN_FIELDS_FLOOR, Math.floor(value)); + } + if (typeof value === 'string') { + const parsed = Number.parseInt(value, 10); + if (Number.isFinite(parsed)) { + return Math.max(MIN_FIELDS_FLOOR, parsed); + } + } + return MIN_FIELDS_FLOOR; +} + +function validateMinFieldsToShow(value: unknown): string | undefined { + if (value === undefined || value === '' || value === null) { + return 'Please specify a minimum number of fields.'; + } + const parsed = + typeof value === 'number' ? value : Number.parseInt(String(value), 10); + if (!Number.isFinite(parsed)) { + return 'Please enter a whole number.'; + } + if (parsed < MIN_FIELDS_FLOOR) { + return `The minimum is ${MIN_FIELDS_FLOOR}.`; + } + return undefined; +} + export default function ConfigScreen({ ctx }: Props) { return ( initialValues={normalizeGlobalParams(ctx.plugin.attributes.parameters)} - onSubmit={async (values: ValidGlobalParams) => { - await ctx.updatePluginParameters(values); + onSubmit={async (values) => { + const normalized: ValidGlobalParams = { + paramsVersion: '2', + startOpen: Boolean(values.startOpen), + minFieldsToShow: parseMinFieldsToShow(values.minFieldsToShow), + }; + await ctx.updatePluginParameters(normalized); ctx.notice('Settings updated successfully!'); }} > {({ handleSubmit, submitting, dirty }) => (
- - {({ input, meta: { error } }) => ( + + value === '' || value === undefined + ? value + : parseMinFieldsToShow(value) + } + > + {({ input, meta: { error, touched } }) => ( )} From 8bfdc733ecea5dcbd7f616dab8daeb91255fd42d Mon Sep 17 00:00:00 2001 From: "Roger Tuan (DatoCMS)" Date: Thu, 7 May 2026 17:19:32 -0700 Subject: [PATCH 2/9] Add missing Spanish UI translations to zoned-datetime-picker The plugin's README advertised Spanish (es) as a supported language but `UILABELS_BY_COUNTRY` in `src/i18n/uiLabels.ts` only contained 7 of the 8 languages, so Spanish editors saw the silent English fallback. Add the missing `es` entry with translations for `suggested`, `browser`, `site`, `dateTime`, and `timeZone`. Co-Authored-By: Claude Opus 4.7 (1M context) --- zoned-datetime-picker/README.md | 1 + zoned-datetime-picker/package-lock.json | 16 ++++++++++++++-- zoned-datetime-picker/package.json | 2 +- zoned-datetime-picker/src/i18n/uiLabels.ts | 7 +++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/zoned-datetime-picker/README.md b/zoned-datetime-picker/README.md index bca69d7d..9697905c 100644 --- a/zoned-datetime-picker/README.md +++ b/zoned-datetime-picker/README.md @@ -86,6 +86,7 @@ The plugin supports localized datetime entry and time zone names in the followin ## Changelog +- 0.1.7 - Added the missing Spanish (es) UI translations that the README already advertised. - 0.1.6 - Moved plugin to official DatoCMS plugins repository. This was just an organizational change and does not add any features or fixes. - 0.1.5 - Minor package updates and readme clarifications - 0.1.4 - Minor bug fixes diff --git a/zoned-datetime-picker/package-lock.json b/zoned-datetime-picker/package-lock.json index 2ea58d17..ddc73f94 100644 --- a/zoned-datetime-picker/package-lock.json +++ b/zoned-datetime-picker/package-lock.json @@ -1,12 +1,12 @@ { "name": "datocms-plugin-zoned-datetime-picker", - "version": "0.1.6", + "version": "0.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "datocms-plugin-zoned-datetime-picker", - "version": "0.1.6", + "version": "0.1.7", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", @@ -57,6 +57,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -402,6 +403,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -445,6 +447,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1046,6 +1049,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.9.tgz", "integrity": "sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.6", "@mui/core-downloads-tracker": "^7.3.9", @@ -1156,6 +1160,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.9.tgz", "integrity": "sha512-aL1q9am8XpRrSabv9qWf5RHhJICJql34wnrc1nz0MuOglPRYF/liN+c8VqZdTvUn9qg+ZjRVbKf4sJVFfIDtmg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.6", "@mui/private-theming": "^7.3.9", @@ -1767,6 +1772,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1877,6 +1883,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2377,6 +2384,7 @@ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" } @@ -2491,6 +2499,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2549,6 +2558,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2558,6 +2568,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2834,6 +2845,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/zoned-datetime-picker/package.json b/zoned-datetime-picker/package.json index 769aea32..bb093ecb 100644 --- a/zoned-datetime-picker/package.json +++ b/zoned-datetime-picker/package.json @@ -3,7 +3,7 @@ "description": "Zoned DateTime Picker is a DatoCMS plugin that stores datetime timestamps with time zone information. Outputs a JSON with ISO8601 and IXDTF strings, the IANA time zone string, UTC offset, and more.", "homepage": "https://github.com/datocms/plugins/tree/master/zoned-datetime-picker", "private": false, - "version": "0.1.6", + "version": "0.1.7", "author": "DatoCMS ", "type": "module", "keywords": [ diff --git a/zoned-datetime-picker/src/i18n/uiLabels.ts b/zoned-datetime-picker/src/i18n/uiLabels.ts index cd5d6c31..d361225d 100644 --- a/zoned-datetime-picker/src/i18n/uiLabels.ts +++ b/zoned-datetime-picker/src/i18n/uiLabels.ts @@ -47,6 +47,13 @@ const UILABELS_BY_COUNTRY: Record = { dateTime: 'Data e hora', timeZone: 'Fuso horário', }, + es: { + suggested: 'Sugeridas', + browser: 'Tu navegador', + site: 'Este proyecto', + dateTime: 'Fecha y hora', + timeZone: 'Zona horaria', + }, cs: { suggested: 'Doporučené', browser: 'Váš prohlížeč', From 3c55b3a4a10dc078fb2e8ad2b4e311bc414c4af7 Mon Sep 17 00:00:00 2001 From: "Roger Tuan (DatoCMS)" Date: Thu, 7 May 2026 18:03:58 -0700 Subject: [PATCH 3/9] Fix lorem-ipsum blockquote rejected as invalid structured text The blockquote that the dummy-text generator produced contained inline text children, but DAST requires `Blockquote.children` to be `Paragraph[]`. The editor flashed "Format not valid" and the API rejected the save. Wrap the blockquote body in a paragraph (`t('blockquote', t('p', s(4)))`) so the value validates and saves. Also clean up pre-existing TS errors that blocked the build: broaden `t()` to accept and flatten the nested arrays `sentences()` produces, add an extra `toStructuredText` overload, pull `react-select` in as a direct dependency, and fix a couple of `as const`/icon type mismatches in `main.tsx`. Co-Authored-By: Claude Opus 4.7 (1M context) --- lorem-ipsum/README.md | 4 ++ lorem-ipsum/package-lock.json | 78 +++++++++++++--------- lorem-ipsum/package.json | 5 +- lorem-ipsum/src/main.tsx | 12 ++-- lorem-ipsum/src/utils/generateDummyText.ts | 4 +- lorem-ipsum/src/utils/text.ts | 24 +++++-- 6 files changed, 80 insertions(+), 47 deletions(-) diff --git a/lorem-ipsum/README.md b/lorem-ipsum/README.md index 6d741ad0..5f6feea6 100644 --- a/lorem-ipsum/README.md +++ b/lorem-ipsum/README.md @@ -5,3 +5,7 @@ Makes it easier to automatically fill your textual fields with dummy content. ## Configuration You can either hook this plugin manually to your text fields (single-line, multi-paragraph, Structured Text), or automatically specifying a number of match rules. + +## Changelog + +- 0.2.6 - Fixed generated structured-text dummy content failing field validation with "Format not valid". The blockquote node was being emitted with inline text children, but the DatoCMS [structured-text spec](https://www.datocms.com/docs/structured-text/dast#blockquote) requires `Blockquote.children` to be `Paragraph[]`. The generator now wraps the blockquote body in a paragraph (`t('blockquote', t('p', s(4)))`) so the value passes validation and saves cleanly. Also tightened the `t()` helper to accept (and flatten) the deeply-nested arrays that `sentences()` produces, added an extra `toStructuredText` overload, pulled in `react-select` as a direct dependency, and corrected a few `as const`/icon types so the project type-checks again. diff --git a/lorem-ipsum/package-lock.json b/lorem-ipsum/package-lock.json index 6aec9e89..10876d77 100644 --- a/lorem-ipsum/package-lock.json +++ b/lorem-ipsum/package-lock.json @@ -1,12 +1,12 @@ { "name": "datocms-plugin-lorem-ipsum", - "version": "0.2.5", + "version": "0.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "datocms-plugin-lorem-ipsum", - "version": "0.2.5", + "version": "0.2.6", "dependencies": { "@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0", @@ -22,7 +22,8 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-final-form": "^7.0.0", - "react-final-form-arrays": "^4.0.0" + "react-final-form-arrays": "^4.0.0", + "react-select": "^5.10.2" }, "devDependencies": { "@types/intersperse": "^1.0.3", @@ -63,6 +64,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -964,6 +966,7 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz", "integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==", "license": "MIT", + "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" }, @@ -1487,6 +1490,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1596,6 +1600,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1684,6 +1689,15 @@ "node": ">=10" } }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -1776,27 +1790,6 @@ "react-dom": ">=17.0.0" } }, - "node_modules/datocms-react-ui/node_modules/react-select": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", - "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.0", - "@emotion/cache": "^11.4.0", - "@emotion/react": "^11.8.1", - "@floating-ui/dom": "^1.0.1", - "@types/react-transition-group": "^4.4.0", - "memoize-one": "^6.0.0", - "prop-types": "^15.6.0", - "react-transition-group": "^4.3.0", - "use-isomorphic-layout-effect": "^1.2.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/datocms-structured-text-slate-utils": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/datocms-structured-text-slate-utils/-/datocms-structured-text-slate-utils-5.1.8.tgz", @@ -1996,6 +1989,7 @@ "resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-4.0.0.tgz", "integrity": "sha512-ya1mbjTmEcmZ8ettYHp/eB4UpfuSjpLJ+grpaQsgxqi1P0wrm+syr/7qLbbslxNLSDA1zoSUthWVCB1O6B9fqg==", "license": "MIT", + "peer": true, "peerDependencies": { "final-form": "^5.0.0" } @@ -2362,6 +2356,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2414,6 +2409,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2423,6 +2419,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2435,6 +2432,7 @@ "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-7.0.0.tgz", "integrity": "sha512-aEeAWbSsCLVXa4GBkJtjjyhPyX4L/Pgp5P/jXZwdz0YYcK6Zs/0PkgB+qWMSyIsbbGGE7m9yYlSpui5E5Gx26A==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4" }, @@ -2493,6 +2491,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-select": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", + "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -2636,6 +2655,7 @@ "resolved": "https://registry.npmjs.org/slate/-/slate-0.82.0.tgz", "integrity": "sha512-2O2NQunBoIWp3M6HP9w1RnNYR6eC52jW4cAXiUDthTxeiwTjL+wrSncls+9jmfqVrUnUb13qbgOEmw6/EdE1yA==", "license": "MIT", + "peer": true, "dependencies": { "immer": "^9.0.6", "is-plain-object": "^5.0.0", @@ -2813,6 +2833,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -2904,15 +2925,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" - }, - "node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } } } } diff --git a/lorem-ipsum/package.json b/lorem-ipsum/package.json index fe012990..196b0ffd 100644 --- a/lorem-ipsum/package.json +++ b/lorem-ipsum/package.json @@ -1,6 +1,6 @@ { "name": "datocms-plugin-lorem-ipsum", - "version": "0.2.5", + "version": "0.2.6", "description": "Fill your textual fields with dummy content", "keywords": [ "datocms-plugin" @@ -31,7 +31,8 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-final-form": "^7.0.0", - "react-final-form-arrays": "^4.0.0" + "react-final-form-arrays": "^4.0.0", + "react-select": "^5.10.2" }, "scripts": { "build": "tsc -b && vite build", diff --git a/lorem-ipsum/src/main.tsx b/lorem-ipsum/src/main.tsx index 2f148ba2..2db55628 100644 --- a/lorem-ipsum/src/main.tsx +++ b/lorem-ipsum/src/main.tsx @@ -1,5 +1,6 @@ import { connect, + type DropdownAction, type ExecuteFieldDropdownActionCtx, type Field, type FieldDropdownActionsCtx, @@ -9,13 +10,16 @@ import { render } from './utils/render'; import 'datocms-react-ui/styles.css'; import generateDummyText from './utils/generateDummyText'; -const loremIpsumDropdownAction = [ +// Returns a fresh, mutable array each time so the SDK's `(DropdownAction | +// DropdownActionGroup)[]` return type stays satisfied — `as const` would mark +// this as `readonly` and fail the type check. +const loremIpsumDropdownAction = (): DropdownAction[] => [ { id: 'loremIpsum', label: 'Generate dummy text', icon: 'font', }, -] as const; +]; // Checks if any auto-apply rule matches the given field, returning the action if so function checkAutoApplyRules( @@ -36,7 +40,7 @@ function checkAutoApplyRules( try { const regex = new RegExp(rule.apiKeyRegexp); if (regex.test(field.attributes.api_key)) { - return loremIpsumDropdownAction; + return loremIpsumDropdownAction(); } } catch (_e) { // If regex is invalid, skip this rule @@ -74,7 +78,7 @@ connect({ } if (hasLoremIpsumAddon(field)) { - return loremIpsumDropdownAction; + return loremIpsumDropdownAction(); } return []; diff --git a/lorem-ipsum/src/utils/generateDummyText.ts b/lorem-ipsum/src/utils/generateDummyText.ts index 26344766..e4006560 100644 --- a/lorem-ipsum/src/utils/generateDummyText.ts +++ b/lorem-ipsum/src/utils/generateDummyText.ts @@ -43,7 +43,7 @@ function article(buttons: string[]) { t('ul', ...times(3).map(() => t('li', t('p', s(rand(1, 3)))))), generateBlockquote && t('h2', title()), generateBlockquote && t('p', s()), - generateBlockquote && t('blockquote', s(4)), + generateBlockquote && t('blockquote', t('p', s(4))), ].filter((x) => !!x) as Tag[]; } @@ -52,7 +52,7 @@ function article(buttons: string[]) { generateList && t('ul', ...times(3).map(() => t('li', t('p', s(rand(1, 3)))))), generateList && t('p', s()), - generateBlockquote && t('blockquote', s(4)), + generateBlockquote && t('blockquote', t('p', s(4))), generateBlockquote && t('p', s()), ].filter((x) => !!x) as Tag[]; } diff --git a/lorem-ipsum/src/utils/text.ts b/lorem-ipsum/src/utils/text.ts index c4c8f70c..31920d06 100644 --- a/lorem-ipsum/src/utils/text.ts +++ b/lorem-ipsum/src/utils/text.ts @@ -26,9 +26,17 @@ export function rand(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } -// Creates a Tag object with provided children -export function t(tag: string, ...children: Array): Tag { - return { tag, children }; +// `sentences()` returns deeply nested arrays of strings/Tag objects (each +// sentence is itself an array, with ' ' separators interspersed). Allowing +// arbitrary nesting in `t()` and flattening at construction time keeps the +// callers in `article()` ergonomic — `t('p', sentences(2, []))` works +// without the caller having to spread the inner arrays. +type TagChild = Tag | string | TagChildList; +interface TagChildList extends ReadonlyArray {} + +export function t(tag: string, ...children: TagChild[]): Tag { + const flat = (children as unknown[]).flat(Infinity) as Array; + return { tag, children: flat }; } // Generates a short sentence that can serve as a title @@ -81,7 +89,7 @@ export function sentences(count: number, buttons: string[]) { } // Converts a tree of Tags into HTML string -export function toHtml(tree: Tag | Tag[] | string): string { +export function toHtml(tree: Tag | Array | string): string { if (typeof tree === 'string') { return tree; } @@ -100,7 +108,7 @@ export function toHtml(tree: Tag | Tag[] | string): string { } // Converts a tree of Tags into Markdown string -export function toMarkdown(tree: Tag | Tag[] | string): string { +export function toMarkdown(tree: Tag | Array | string): string { if (typeof tree === 'string') { return tree; } @@ -171,6 +179,7 @@ export function url() { export function toStructuredText(tree: Tag): Node; export function toStructuredText(tree: Array): Node[]; export function toStructuredText(tree: string): Node; +export function toStructuredText(tree: Tag | string): Node; /** * Recursively transforms a Tag-based tree into a DatoCMS-structured-text format. @@ -186,7 +195,10 @@ export function toStructuredText( } if (Array.isArray(tree)) { - return tree.flatMap(toStructuredText); + // Wrap in an arrow so the call resolves the right overload — passing + // `toStructuredText` directly to flatMap confuses TS about which signature + // applies to a `string | Tag` element. + return tree.flatMap((node) => toStructuredText(node as Tag | string)); } const childNodes = toStructuredText(tree.children); From a8a455b206ddcac834f34b00bb364795da497ca2 Mon Sep 17 00:00:00 2001 From: "Roger Tuan (DatoCMS)" Date: Thu, 7 May 2026 18:14:35 -0700 Subject: [PATCH 4/9] Make inverse-relationships configurable and zero-config-friendly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inverse-relationships sidebar panel was never appearing because the required `itemTypeApiKey`, `fieldApiKey`, `orderBy`, and `limit` parameters were declared as `instance` parameters (per-field) but the runtime read them from `ctx.plugin.attributes.parameters` (global), and the plugin no longer registers any field extension to attach instance params to. Promote them to `global` parameters so the built-in plugin settings UI exposes them. Drop the `required: true` from `datoCmsApiToken` and clarify in its hint that it's optional (the SidebarPanel already falls back to `ctx.currentUserAccessToken`). Declare `"permissions": ["currentUserAccessToken"]` in the manifest so that fallback actually works — without the permission the SDK leaves `ctx.currentUserAccessToken` undefined and the auto-token flow silently fails. Also drop the broken `/admin/access_tokens` link from the hint. Add `base: './'` to `vite.config.ts` so the bundled `index.html` references its assets relatively, matching the other plugins in the repo and avoiding 404s when served under the per-plugin subdirectory. Co-Authored-By: Claude Opus 4.7 (1M context) --- inverse-relationships/README.md | 11 +++++-- inverse-relationships/package-lock.json | 30 ++++++++++++------- inverse-relationships/package.json | 38 +++++++++++++------------ inverse-relationships/vite.config.ts | 2 ++ 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/inverse-relationships/README.md b/inverse-relationships/README.md index cd5b57e6..c2a8e430 100644 --- a/inverse-relationships/README.md +++ b/inverse-relationships/README.md @@ -1,13 +1,18 @@ # DatoCMS Inverse Relationships -A simple plugin that displays inverse relationships on a record's page (ie. blog posts by a specific author). +A simple plugin that displays inverse relationships in a record's sidebar (ie. blog posts by a specific author). ## Configuration -Please specify a read-only DatoCMS API key on the plugin global settings: +In the plugin settings, specify the model whose records should be listed, plus the single-link or multiple-links field that connects them back to the current record. You can also configure the ordering and the maximum number of results to display. A read-only DatoCMS API token can optionally be provided; otherwise the plugin uses the editor's current access token. ![Demo](https://raw.githubusercontent.com/datocms/plugins/master/inverse-relationships/docs/global.png) -When applying this plugin to your field, please insert the following settings: +Once configured, an "Inverse relationships" panel will automatically appear in the sidebar of every record, listing the matching linked records with quick links to open them. ![Demo](https://raw.githubusercontent.com/datocms/plugins/master/inverse-relationships/docs/instance.png) + +## Changelog + +- 0.1.11 - Declared `"permissions": ["currentUserAccessToken"]` in the plugin manifest so `ctx.currentUserAccessToken` is actually populated at runtime. Without it, the optional zero-config flow added in 0.1.10 always fell back to an empty token because the SDK leaves `currentUserAccessToken` `undefined` when the permission isn't requested. +- 0.1.10 - Fixed the plugin being effectively unconfigurable, which made the inverse-relationships sidebar panel never appear. The required `itemTypeApiKey`, `fieldApiKey`, `orderBy`, and `limit` parameters were declared as `instance` parameters (per-field) but the runtime read them from `ctx.plugin.attributes.parameters` (global) and the plugin no longer registers any field extension to attach them to. Promoted them to `global` parameters so the built-in plugin settings UI exposes them. The `datoCmsApiToken` is now optional — the SidebarPanel already falls back to `ctx.currentUserAccessToken` — and its hint no longer points to the broken `/admin/access_tokens` link. Also added `base: './'` to the Vite config so the bundled `index.html` references its assets relatively, matching the other plugins in this repo. diff --git a/inverse-relationships/package-lock.json b/inverse-relationships/package-lock.json index 90eb52ed..7d570e65 100644 --- a/inverse-relationships/package-lock.json +++ b/inverse-relationships/package-lock.json @@ -1,12 +1,12 @@ { "name": "datocms-plugin-inverse-relationships", - "version": "0.1.9", + "version": "0.1.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "datocms-plugin-inverse-relationships", - "version": "0.1.9", + "version": "0.1.11", "license": "ISC", "dependencies": { "@datocms/cma-client-browser": "^5.3.0", @@ -58,6 +58,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1400,6 +1401,7 @@ "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -1421,6 +1423,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1531,6 +1534,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1610,6 +1614,15 @@ "node": ">=10" } }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2128,6 +2141,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2180,6 +2194,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2189,6 +2204,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2487,6 +2503,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -2562,15 +2579,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" - }, - "node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } } } } diff --git a/inverse-relationships/package.json b/inverse-relationships/package.json index e2768838..ed138c8d 100644 --- a/inverse-relationships/package.json +++ b/inverse-relationships/package.json @@ -1,7 +1,7 @@ { "name": "datocms-plugin-inverse-relationships", "homepage": "https://github.com/datocms/plugins/tree/master/inverse-relationships#readme", - "version": "0.1.9", + "version": "0.1.11", "description": "A simple plugin that displays inverse relationships on a record's page", "author": "DatoCMS ", "license": "ISC", @@ -32,45 +32,47 @@ "fieldTypes": [ "json" ], + "permissions": [ + "currentUserAccessToken" + ], "parameters": { "global": [ - { - "id": "datoCmsApiToken", - "label": "DatoCMS API Token", - "type": "string", - "required": true, - "hint": "The DatoCMS API read-only token to use to query inverse relationships, get it here" - } - ], - "instance": [ { "id": "itemTypeApiKey", - "label": "Model ID", + "label": "Model API key", "type": "string", "required": true, - "hint": "The model you want linked records to show up (ie. post)" + "hint": "API key of the model whose records should appear in the sidebar (ie. post)" }, { "id": "fieldApiKey", - "label": "Field ID", + "label": "Link field API key", "type": "string", "required": true, - "hint": "The single-link field to use as foreign key (ie. author)" + "hint": "API key of the single-link or multiple-links field on that model that points back to the current record (ie. author)" }, { "id": "orderBy", "label": "Order by", "type": "string", - "required": true, - "default": "_updated_at_DESC" + "required": false, + "default": "_updated_at_DESC", + "hint": "Optional sort expression supported by the CMA, e.g. _updated_at_DESC" }, { "id": "limit", "label": "Number of results", "type": "integer", - "required": true, - "hint": "The maximum number of records to show", + "required": false, + "hint": "Maximum number of related records to show in the sidebar", "default": 10 + }, + { + "id": "datoCmsApiToken", + "label": "DatoCMS API token (optional)", + "type": "string", + "required": false, + "hint": "Read-only API token used to query inverse relationships. Leave empty to reuse the editor's current access token." } ] } diff --git a/inverse-relationships/vite.config.ts b/inverse-relationships/vite.config.ts index fabde1a8..86a5fa03 100644 --- a/inverse-relationships/vite.config.ts +++ b/inverse-relationships/vite.config.ts @@ -1,6 +1,8 @@ import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; +// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + base: './', }); From e6b563fef26700f5ccdcca279dec053fb3b7ec87 Mon Sep 17 00:00:00 2001 From: "Roger Tuan (DatoCMS)" Date: Thu, 7 May 2026 18:22:58 -0700 Subject: [PATCH 5/9] Fix asset-optimization decimal MB threshold and preview View asset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bundled preview-mode fixes: * The MB→bytes conversion (`largeAssetThreshold * 1024 * 1024`) produced a float for any non-integer MB value (e.g. 0.1 MB → 104857.6 bytes), which the CMA `size` filter rejected with `INVALID_FILTER_FIELDS_PARAM` ("Could not coerce value 0.10485760 to IntType"). Floor the threshold before passing it into the filter. * The "View Asset" button on each row of the Optimized Assets panel always opened the unmodified original. After a preview run the asset hasn't been rewritten yet, so editors thought the optimization didn't apply. Plumb the dry-run Imgix URL through to `OptimizedAsset`, pass `isPreview` down to `AssetList`, and switch the button to "View optimized preview" + open that URL when the panel is showing dry-run results. Hide the "Media Area" button in that case too — it would just take editors to the still-unmodified asset and reinforce the same misconception. Co-Authored-By: Claude Opus 4.7 (1M context) --- asset-optimization/README.md | 7 ++ asset-optimization/package-lock.json | 11 ++- asset-optimization/package.json | 2 +- .../asset-optimization/AssetList.tsx | 75 +++++++++++++++---- .../src/entrypoints/OptimizeAssetsPage.tsx | 25 ++++++- .../src/utils/optimizationUtils.ts | 6 ++ 6 files changed, 104 insertions(+), 22 deletions(-) diff --git a/asset-optimization/README.md b/asset-optimization/README.md index 029805cf..d7f84df8 100644 --- a/asset-optimization/README.md +++ b/asset-optimization/README.md @@ -100,3 +100,10 @@ If you encounter any issues or have questions about the plugin, please [open an ## License MIT + +## Changelog + +- 0.7.9 - Hide the **Media Area** button on optimized rows during preview runs. In dry-run mode the asset hasn't been written back to the media library, so jumping into it would just show the original and reinforce the misconception that the optimization already ran. The button still appears for skipped/failed entries and for real (non-preview) optimization runs. +- 0.7.8 - Two preview-mode fixes: + - Decimal megabyte thresholds (e.g. `0.1` MB) no longer break the asset listing. The plugin converts MB to bytes (multiplying by `1024 * 1024`) and the result was being passed to the CMA `size` filter as a float, which the API rejected with `INVALID_FILTER_FIELDS_PARAM` ("Could not coerce value `0.10485760` to IntType"). The threshold is now floored to a whole number of bytes before the request goes out. + - The "View Asset" button in the **Optimized Assets** panel after a preview run was opening the unmodified original (because preview runs never replace anything), making editors think the optimization didn't apply. The dry-run pipeline now keeps hold of the Imgix URL it generated for the preview, and the button switches to "View optimized preview" + opens that URL when the panel is showing dry-run results. diff --git a/asset-optimization/package-lock.json b/asset-optimization/package-lock.json index cf605b0d..52029c24 100644 --- a/asset-optimization/package-lock.json +++ b/asset-optimization/package-lock.json @@ -1,12 +1,12 @@ { "name": "datocms-plugin-asset-optimization", - "version": "0.7.7", + "version": "0.7.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "datocms-plugin-asset-optimization", - "version": "0.7.7", + "version": "0.7.9", "dependencies": { "@datocms/cma-client-browser": "^5.3.0", "datocms-plugin-sdk": "^2.1.1", @@ -56,6 +56,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1426,6 +1427,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1536,6 +1538,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2127,6 +2130,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2179,6 +2183,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2188,6 +2193,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2500,6 +2506,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/asset-optimization/package.json b/asset-optimization/package.json index a5cb59ad..89982a9b 100644 --- a/asset-optimization/package.json +++ b/asset-optimization/package.json @@ -1,7 +1,7 @@ { "name": "datocms-plugin-asset-optimization", "homepage": "https://github.com/datocms/plugins/tree/master/asset-optimization#readme", - "version": "0.7.7", + "version": "0.7.9", "author": "DatoCMS ", "description": "A plugin that allows you to mass apply optimizations to your DatoCMS assets.", "keywords": [ diff --git a/asset-optimization/src/components/asset-optimization/AssetList.tsx b/asset-optimization/src/components/asset-optimization/AssetList.tsx index acfc46c5..1c4be434 100644 --- a/asset-optimization/src/components/asset-optimization/AssetList.tsx +++ b/asset-optimization/src/components/asset-optimization/AssetList.tsx @@ -12,6 +12,15 @@ import type { interface AssetListProps { assets: Asset[] | OptimizedAssetType[] | ProcessedAsset[]; category: 'optimized' | 'skipped' | 'failed'; + /** + * True when the parent page is showing results from a "Preview Optimization" + * (dry-run) pass — no assets were modified and the original asset URL still + * points at the unmodified bytes. The "View Asset" button switches to the + * Imgix preview URL in this mode so editors can actually compare the + * predicted optimized output instead of being silently routed back to the + * original. + */ + isPreview?: boolean; onClose?: () => void; ctx: RenderPageCtx; } @@ -20,6 +29,7 @@ interface AssetListProps { interface DisplayAsset extends Asset { originalSize?: number; optimizedSize?: number; + optimizedUrl?: string; } const getCategoryBadgeClass = ( @@ -44,6 +54,8 @@ const normalizeToDisplayAsset = ( 'originalSize' in asset ? (asset.originalSize as number) : undefined; const optimizedSize = 'optimizedSize' in asset ? (asset.optimizedSize as number) : undefined; + const optimizedUrl = + 'optimizedUrl' in asset ? (asset.optimizedUrl as string | undefined) : undefined; const size = 'size' in asset ? asset.size : (originalSize ?? 0); const basename = 'basename' in asset ? asset.basename : ''; @@ -56,6 +68,7 @@ const normalizeToDisplayAsset = ( basename, originalSize, optimizedSize, + optimizedUrl, }; }; @@ -72,12 +85,14 @@ const computeSavingsPercentage = ( type AssetListItemProps = { asset: Asset | OptimizedAssetType | ProcessedAsset; category: 'optimized' | 'skipped' | 'failed'; + isPreview: boolean; ctx: RenderPageCtx; }; const AssetListItem = ({ asset, category, + isPreview, ctx, }: AssetListItemProps): ReactElement => { const displayAsset = normalizeToDisplayAsset(asset); @@ -91,6 +106,20 @@ const AssetListItem = ({ Boolean(displayAsset.originalSize) && Boolean(displayAsset.optimizedSize); + // In preview mode the asset URL is still the unmodified original — opening + // it from the "View Asset" button just shows the pre-optimization image and + // confused editors into thinking the optimization didn't apply. Prefer the + // dry-run Imgix URL when we have one so the button actually surfaces the + // predicted output. + const showOptimizedPreview = + isPreview && category === 'optimized' && Boolean(displayAsset.optimizedUrl); + const viewAssetLabel = showOptimizedPreview + ? 'View optimized preview' + : 'View Asset'; + const viewAssetUrl = showOptimizedPreview + ? (displayAsset.optimizedUrl as string) + : displayAsset.url; + return (
  • @@ -128,23 +157,33 @@ const AssetListItem = ({ - + {/* + * The Media Area link is hidden in preview mode for optimized + * entries: nothing has been written back to the media library yet, + * so jumping into it would just show the original asset and + * reinforce the misconception that the optimization already ran. + * It still appears for skipped/failed entries (where the media + * library still has useful context) and for non-preview runs. + */} + {!showOptimizedPreview && ( + + )}
  • @@ -155,11 +194,14 @@ const AssetListItem = ({ * AssetList component displays categorized lists of assets (optimized, skipped, or failed) * * This component displays a list of assets based on the selected category with action buttons: - * - "View Asset" button - Opens the asset in a new tab + * - "View Asset" / "View optimized preview" button - opens the asset (or, in + * preview mode for optimized entries, the dry-run Imgix URL) in a new tab + * - "Media Area" button - jumps to the asset in the DatoCMS media library */ const AssetList = ({ assets, category, + isPreview = false, onClose, ctx, }: AssetListProps): ReactElement => { @@ -193,6 +235,7 @@ const AssetList = ({ key={asset.id} asset={asset} category={category} + isPreview={isPreview} ctx={ctx} /> ))} diff --git a/asset-optimization/src/entrypoints/OptimizeAssetsPage.tsx b/asset-optimization/src/entrypoints/OptimizeAssetsPage.tsx index 94c9e468..6afa1605 100644 --- a/asset-optimization/src/entrypoints/OptimizeAssetsPage.tsx +++ b/asset-optimization/src/entrypoints/OptimizeAssetsPage.tsx @@ -65,11 +65,13 @@ function assetToOptimizedAsset( asset: Asset, originalSize: number, optimizedSize: number, + optimizedUrl?: string, ): OptimizedAsset { return { id: asset.id, path: asset.path, url: asset.url, + optimizedUrl, originalSize, optimizedSize, }; @@ -112,6 +114,7 @@ async function processAsset( status: 'optimized' | 'skipped' | 'failed'; asset: Asset; optimizedSize?: number; + optimizedUrl?: string; error?: string; }> { try { @@ -152,13 +155,17 @@ async function processAsset( return { status: 'skipped', asset }; } - // If this is just a preview, don't actually replace the asset + // If this is just a preview, don't actually replace the asset. Return the + // Imgix URL so the UI can let the user inspect the predicted optimized + // version directly — without a real preview URL the "View Asset" button + // would only ever open the still-unmodified original. if (isPreview) { addSizeComparisonLog(asset.path, asset.size, optimizedImageBlob.size); return { status: 'optimized', asset, optimizedSize: optimizedImageBlob.size, + optimizedUrl, }; } @@ -232,11 +239,17 @@ async function fetchOptimizableAssets( const assets: Asset[] = []; let count = 0; + // The CMA `size` filter expects an integer (number of bytes); a fractional + // megabyte threshold like 0.1 MB → 104857.6 bytes makes the API reject the + // query with `INVALID_FILTER_FIELDS_PARAM` ("Could not coerce value 0.10485760 + // to IntType"). Floor here so users can keep entering decimal MB values. + const sizeThresholdBytes = Math.floor(largeAssetThresholdBytes); + for await (const upload of client.uploads.listPagedIterator({ filter: { fields: { type: { eq: 'image' }, - size: { gte: largeAssetThresholdBytes }, + size: { gte: sizeThresholdBytes }, }, }, })) { @@ -302,7 +315,12 @@ async function processPageQueueTask({ if (result.status === 'optimized' && result.optimizedSize) { acc.optimizedAssets.push( - assetToOptimizedAsset(asset, asset.size, result.optimizedSize), + assetToOptimizedAsset( + asset, + asset.size, + result.optimizedSize, + result.optimizedUrl, + ), ); acc.optimizedSizeTotal += result.optimizedSize; acc.optimized++; @@ -761,6 +779,7 @@ const OptimizeAssetsPage = ({ ctx }: Props) => { : result.failedAssets } category={selectedCategory} + isPreview={isPreviewing} onClose={() => setSelectedCategory(null)} ctx={ctx} /> diff --git a/asset-optimization/src/utils/optimizationUtils.ts b/asset-optimization/src/utils/optimizationUtils.ts index 67e11e2c..96db54d1 100644 --- a/asset-optimization/src/utils/optimizationUtils.ts +++ b/asset-optimization/src/utils/optimizationUtils.ts @@ -30,6 +30,12 @@ export interface OptimizedAsset { id: string; path: string; url: string; + /** + * Imgix URL with the optimization parameters applied. Populated only on + * preview runs so the UI can offer a "view what this would look like" link + * without dereferencing the original (still-unmodified) asset URL. + */ + optimizedUrl?: string; originalSize: number; optimizedSize: number; } From 50c5d4e6f160e6a6e852d2b5f5b56f44670022d2 Mon Sep 17 00:00:00 2001 From: "Roger Tuan (DatoCMS)" Date: Thu, 7 May 2026 18:25:55 -0700 Subject: [PATCH 6/9] Fix todo-list bundle referencing assets via absolute paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without `base: './'` in `vite.config.ts`, Vite emits the bundled `index.html` with `