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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/bump-cloudinary-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'next-tinacms-cloudinary': patch
---

Bump `cloudinary` to `^2.7.0`

The catalog pinned `^1.41.3`, which carried GHSA-g4mf-96x5-5m2c (argument injection via ampersand, patched in 2.7.0) and pulled in the deprecated `q` promise library. v2 drops `q`, `core-js` and `cloudinary-core`, leaving `lodash` as its only dependency.

No code changes were needed: the package already imports the v2 API, and every method it uses (`config`, `uploader.upload`, `uploader.destroy`, `search`, `api.root_folders`, `api.sub_folders`) is unchanged. Of v2.0.0's three breaking changes, `secure` defaulting to true is already set explicitly, URL analytics only affects SDK-generated URLs rather than the Search API response fields this package reads, and the dropped Node 6/8 support is long past.
10 changes: 10 additions & 0 deletions .changeset/bump-final-form-v5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'tinacms': patch
'@tinacms/app': patch
---

Bump the final-form family to the TypeScript releases

`final-form` 4.20.10 → ^5.0.1, `final-form-arrays` ^3.1.0 → ^4.0.1, `react-final-form` ^6.5.9 → ^7.0.1. All three majors are the same event: a coordinated Flow → TypeScript rewrite published on 2025-06-07 and labelled as carrying no API changes. `react-final-form@7` is where React 19 was added to the peer range, which clears the last unmet peer warning on install outside the GraphiQL chain.

They must move together because each peers on the next: `react-final-form@7` requires `final-form@^5`, and `final-form-arrays@3` peers on `final-form@^4`. `final-form-set-field-data` stays put — its peer is `>=1.2.0`.
5 changes: 5 additions & 0 deletions .changeset/button-disabled-attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'tinacms': patch
---

Fix `Button` and `IconButton` not passing `disabled` to the DOM. Both components used the prop only to pick styling (`pointer-events-none`), which blocks the pointer but not the keyboard, so every disabled button in the CMS stayed focusable and could still be activated with Enter or Space. `busy` is now treated as disabled too, closing a double-submit path on in-flight buttons. `disabled` is omitted when `Button` renders as a tag that does not support it (`as='a'`).
8 changes: 8 additions & 0 deletions .changeset/drop-crypto-js.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@tinacms/cli': patch
'tinacms': patch
---

Drop the deprecated `crypto-js` dependency

`@tinacms/cli` used it in one place, to generate the default `NEXTAUTH_SECRET` offered during `tinacms init`. `crypto.lib.WordArray.random(16).toString()` is replaced with `randomBytes(16).toString('hex')` from `node:crypto`, which produces the same 32-character hex string. `tinacms` declared the dependency without ever importing it.
7 changes: 7 additions & 0 deletions .changeset/drop-fs-extra-metrics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tinacms/metrics': patch
---

Drop the `fs-extra` peer dependency

`@tinacms/metrics` declared `fs-extra@^9.0.1` as a peer while the repo itself ran `^11.3.0`, so the range was never satisfied and every install printed an unmet-peer warning. The one call site read `package.json` with `readFileSync`, which fs-extra re-exports unchanged from `node:fs`, so the peer is replaced with the builtin and `@types/node` covers the types that previously arrived through `@types/fs-extra`.
10 changes: 10 additions & 0 deletions .changeset/drop-orphaned-dep-entries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'next-tinacms-azure': patch
'next-tinacms-cloudinary': patch
'next-tinacms-dos': patch
'next-tinacms-s3': patch
---

Drop the unused `@types/crypto-js` devDependency

`crypto-js` itself was removed from `tinacms` and `@tinacms/cli`, and no source file in these packages imports it, so the type package had nothing left to type.
8 changes: 8 additions & 0 deletions .changeset/drop-typedoc-configs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@tinacms/graphql': patch
'@tinacms/search': patch
---

Remove the dead typedoc docs tooling

Both packages declared a `docs` script running `pnpm typedoc` without ever declaring `typedoc` as a dependency, so the script could not resolve its binary under pnpm's isolated `node_modules`. Their `typedoc.json` files were also written against the pre-0.20 option schema (`inputFiles`, `mode`, `excludeNotExported`), which the catalog's typedoc 0.26 no longer accepts. Nothing in `turbo.json` or any workflow invoked them, so the scripts and configs are removed along with the generated `spec.md` in `@tinacms/mdx`.
7 changes: 7 additions & 0 deletions .changeset/drop-typedoc-mdx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tinacms/mdx': patch
---

Drop the unused `typedoc` dependency

`typedoc` landed in `@tinacms/mdx`'s runtime dependencies by copy-paste and has shipped to every consumer since, dragging an unmet `typescript` peer range with it. Nothing in `src/` imports it and only the `docs` script used it, so both are removed along with the now-orphaned `typedoc-plugin-markdown` and `concat-md` catalog entries.
9 changes: 9 additions & 0 deletions .changeset/field-props-explicit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'tinacms': patch
---

Declare the field props that `react-final-form`'s index signature used to cover

`FieldRenderProps` carried `[otherProp: string]: any` in v6, so the extras `FieldsBuilder` passes to every field plugin — `tinaForm`, `index`, `children`, `experimental_focusIntent` — type-checked implicitly. v7's TypeScript rewrite dropped that index signature, so they are now declared on `FieldProps` directly. The rich-text plugin's `rawMode`, `setRawMode` and `rawEditor` are declared on its own props rather than the shared type.

No runtime change; these props were always being passed.
13 changes: 13 additions & 0 deletions .changeset/graphiql-off-2023-prerelease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@tinacms/app": patch
---

Move the GraphQL playground off the 2023 `graphiql` pre-release (`3.0.0-alpha.1`) to `^4.1.2`.

The catalog pinned an exact alpha published four days before stable 3.0.0, so no downstream consumer could override it. It carried `@graphiql/react@0.18.0` → `markdown-it@12.3.2` → `linkify-it@3.0.3`, keeping GHSA-6v5v-wf23-fmfq (markdown-it) and GHSA-22p9-wv53-3rq4 / GHSA-v245-v573-v5vm (linkify-it) alive for everyone installing `@tinacms/app`.

Bumping `graphiql` alone was not enough: both `@graphiql/react` and `typedoc` declare `markdown-it: ^14.1.0`, and pnpm deduped that to `14.1.0` — below the `14.1.2` fix. A `markdown-it: ^14.3.0` override resolves both chains to patched versions (`markdown-it@14.3.0`, `linkify-it@5.0.2`).

`graphiql` 5 was evaluated and deliberately not taken: it replaces CodeMirror with a bundled Monaco (undoing the recent 73 MB `monaco-editor` removal, and requiring a `setup-workers` import that changes the Vite build contract for every consumer building `@tinacms/app` from source), and it drops the controlled `query` / `variables` props the playground's "Queries" sidebar depends on. Version 4 keeps CodeMirror and that prop contract, so the migration is the CSS import path plus one latent-bug fix.

`defaultTabs={[]}` is removed. An empty array is a valid-looking but impossible state — zero tabs — and v4 dereferences `tabs[activeTabIndex]` when recording history, so executing any query crashed the playground with "Cannot read properties of undefined (reading 'query')". v3 only survived it because a `??` short-circuit happened to skip the same lookup. Without the prop, GraphiQL creates the single default tab seeded from `query`/`variables`, which is what the playground wanted.
7 changes: 7 additions & 0 deletions .changeset/group-list-test-hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'tinacms': patch
---

Add `data-test` hooks to group-list and blocks field controls

Mirrors the hooks already on simple list fields, so end-to-end tests can target the add button and field wrapper of an object list or a blocks field without depending on Tailwind classes. Nested fields carry their full path, e.g. `add-item-blocks.0.actions`.
9 changes: 9 additions & 0 deletions .changeset/list-field-test-hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'tinacms': patch
---

Add `data-test` hooks to list field controls

The add and delete buttons on `list: true` fields had no stable selector, so end-to-end tests had to target Tailwind classes. Adds `data-test="list-<name>"` on the field wrapper, `data-test="add-item-<name>"` on the add button, and `data-test="delete-item-<name>.<index>"` on the delete button shared with group-list and blocks fields.

Every hook carries the full field path. The wrapper hook lands on the outer field wrapper, so a nested list's delete buttons are descendants of the outer list's wrapper; a bare id would make `[data-test="list-x"] [data-test="delete-item"]` match the wrong row once lists nest.
9 changes: 9 additions & 0 deletions .changeset/mdx-drop-uvu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@tinacms/mdx': patch
---

Stop importing the `uvu` test runner from shipped source

`@tinacms/mdx` listed `uvu` as a runtime dependency and imported it in two shortcode parsing files, in both cases only for `ok` as a one-line assertion helper. A local `assert` function replaces those six call sites, so the dependency and its catalog entry are gone. Shortcode parsing behaviour is unchanged.

This does not remove `uvu` from a consumer's `node_modules`. `micromark` and its extensions still depend on it at runtime, and `@tinacms/mdx` depends on those.
83 changes: 83 additions & 0 deletions examples/next/kitchen-sink/e2e/admin/author-list-field.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { expect, test } from '../fixtures/test-content';
import {
clickSave,
navigateToCreate,
navigateToEdit,
} from '../utils/admin-helpers';
import { deleteDocument } from '../utils/delete-document';

const AUTHOR_NAME = 'e2e list author';
const AUTHOR_FILENAME = 'e2e-list-author';
const AUTHOR_RELATIVE_PATH = `${AUTHOR_FILENAME}.md`;

const HOBBIES = ['reading', 'cycling', 'baking'];

const listValues = (page): Promise<string[]> =>
page
.locator('[data-test="list-hobbies"] input')
.evaluateAll((inputs) => inputs.map((i) => (i as HTMLInputElement).value));

// The delete hook is namespaced by field path, so this addresses the exact row
// rather than relying on descendant order within the wrapper.
const deleteItemAt = (page, index: number) =>
page.locator(`[data-test="delete-item-hobbies.${index}"]`).click();

/**
* Add, remove and persist items on a `list: true` field.
*
* `final-form-arrays` v4 reworked insertion indexing, `removeBatch` ordering and
* empty-array handling. Nothing else in the suite mutates a list field, so this
* walks one document through the states those changes affect, saving and
* reloading between each so the assertions cover the written file rather than
* just in-memory form state.
*/
test.describe('Author list field', () => {
test.beforeAll(async ({ playwright }) => {
const ctx = await playwright.request.newContext({
baseURL: process.env.GRAPHQL_URL ?? 'http://localhost:4001',
extraHTTPHeaders: { 'Content-Type': 'application/json' },
});
try {
await deleteDocument(ctx, 'author', AUTHOR_RELATIVE_PATH);
} catch {
// Document may not exist — that's fine
}
await ctx.dispose();
});

test('adds, removes and empties list items across saves', async ({
page,
contentCleanup,
}) => {
await navigateToCreate(page, 'author');
await page.fill('input[name="name"]', AUTHOR_NAME);
contentCleanup.track('author', AUTHOR_RELATIVE_PATH);

for (const [index, hobby] of HOBBIES.entries()) {
await page.click('[data-test="add-item-hobbies"]');
await page.fill(`input[name="hobbies.${index}"]`, hobby);
}
expect(await listValues(page)).toEqual(HOBBIES);

await clickSave(page);
await navigateToEdit(page, 'author', AUTHOR_FILENAME);
expect(await listValues(page)).toEqual(HOBBIES);

// Removing the middle item must shift the tail up, not duplicate or drop it.
await deleteItemAt(page, 1);
expect(await listValues(page)).toEqual(['reading', 'baking']);

await clickSave(page);
await navigateToEdit(page, 'author', AUTHOR_FILENAME);
expect(await listValues(page)).toEqual(['reading', 'baking']);

// Emptying the list must persist as empty rather than retaining the last value.
await deleteItemAt(page, 0);
await deleteItemAt(page, 0);
expect(await listValues(page)).toEqual([]);

await clickSave(page);
await navigateToEdit(page, 'author', AUTHOR_FILENAME);
expect(await listValues(page)).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { expect, test } from '../fixtures/test-content';
import {
clickSave,
navigateToCreate,
navigateToEdit,
} from '../utils/admin-helpers';
import { deleteDocument } from '../utils/delete-document';

const POST_TITLE = 'Recovered validation post';
const POST_FILENAME = 'recovered-validation-post';
const POST_RELATIVE_PATH = `${POST_FILENAME}.md`;

const VALIDATION_ERROR = 'Title must be at least 5 characters';

const saveButton = (page) => page.locator('button:has-text("Save")');

/**
* Drives a form from invalid to valid and back.
*
* The existing post spec asserts that an invalid title blocks saving. This
* covers the return trip, which is what final-form v5 changed: submit errors
* used to be discarded when sync validation failed after a failed submission,
* and `beforeSubmit` ran after the sync-error check rather than before. A
* regression shows up as a form that stays stuck disabled once corrected, or
* one that saves while still invalid.
*/
test.describe('Post validation recovery', () => {
test.beforeAll(async ({ playwright }) => {
const ctx = await playwright.request.newContext({
baseURL: process.env.GRAPHQL_URL ?? 'http://localhost:4001',
extraHTTPHeaders: { 'Content-Type': 'application/json' },
});
try {
await deleteDocument(ctx, 'post', POST_RELATIVE_PATH);
} catch {
// Document may not exist — that's fine
}
await ctx.dispose();
});

test('recovers from an invalid title and saves', async ({
page,
contentCleanup,
}) => {
await navigateToCreate(page, 'post');

await page.fill('input[name="title"]', 'Hi');
await expect(page.locator(`text=${VALIDATION_ERROR}`)).toBeVisible({
timeout: 5000,
});
await expect(saveButton(page)).toHaveClass(/pointer-events-none/, {
timeout: 3000,
});

// Correcting the field must clear the error and re-enable submission.
await page.fill('input[name="title"]', POST_TITLE);
await expect(page.locator(`text=${VALIDATION_ERROR}`)).toHaveCount(0, {
timeout: 5000,
});
await expect(saveButton(page)).not.toHaveClass(/pointer-events-none/, {
timeout: 5000,
});

contentCleanup.track('post', POST_RELATIVE_PATH);
await clickSave(page);

await navigateToEdit(page, 'post', POST_FILENAME);
await expect(page.locator('input[name="title"]')).toHaveValue(POST_TITLE);

// Validation stays live on a saved document, and recovers a second time.
await page.fill('input[name="title"]', 'Hi');
await expect(page.locator(`text=${VALIDATION_ERROR}`)).toBeVisible({
timeout: 5000,
});

await page.fill('input[name="title"]', POST_TITLE);
await expect(page.locator(`text=${VALIDATION_ERROR}`)).toHaveCount(0, {
timeout: 5000,
});
await expect(saveButton(page)).not.toHaveClass(/pointer-events-none/, {
timeout: 5000,
});
});
});
19 changes: 0 additions & 19 deletions examples/next/kitchen-sink/e2e/admin/post.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,23 +139,4 @@ test.describe('Post CRUD via TinaCMS Admin', () => {
'E2E Playwright Test Post Updated'
);
});

test('should validate post title minimum length', async ({ page }) => {
await navigateToCreate(page, 'post');

// Enter a title shorter than 5 characters — validation runs on change
await page.fill('input[name="title"]', 'Hi');

// Inline validation error appears immediately (no blur/save needed)
const errorMessage = page.locator(
'text=Title must be at least 5 characters'
);
await expect(errorMessage).toBeVisible({ timeout: 5000 });

// The Save button should be disabled (no cursor=pointer)
const saveButton = page.locator('button:has-text("Save")');
await expect(saveButton).toHaveClass(/pointer-events-none/, {
timeout: 3000,
});
});
});
2 changes: 1 addition & 1 deletion examples/next/kitchen-sink/tina/tina-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions packages/@tinacms/app/src/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Folder } from 'lucide-react';
import React from 'react';
import { useCMS } from 'tinacms';

import 'graphiql/graphiql.min.css';
import 'graphiql/style.css';

const Playground = () => {
const cms = useCMS();
Expand Down Expand Up @@ -141,7 +141,6 @@ const Playground = () => {
query={query}
defaultEditorToolsVisibility='variables'
isHeadersEditorEnabled={false}
defaultTabs={[]}
plugins={[
{
title: 'Queries',
Expand Down
2 changes: 0 additions & 2 deletions packages/@tinacms/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"@types/clear": "catalog:",
"@types/cli-spinner": "catalog:",
"@types/cors": "2.8.5",
"@types/crypto-js": "^4.2.2",
"@types/express": "catalog:",
"@types/figlet": "catalog:",
"@types/fs-extra": "^11.0.4",
Expand Down Expand Up @@ -86,7 +85,6 @@
"cli-spinner": "catalog:",
"clipanion": "catalog:",
"cors": "catalog:",
"crypto-js": "catalog:",
"dotenv": "catalog:",
"esbuild": "catalog:",
"fs-extra": "catalog:",
Expand Down
6 changes: 2 additions & 4 deletions packages/@tinacms/cli/src/cmds/init/prompts/authProvider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import crypto from 'crypto-js';
import { randomBytes } from 'node:crypto';
import prompts from 'prompts';

import type { Framework } from '../';
Expand Down Expand Up @@ -68,9 +68,7 @@ const authProviderUpdateConfig: {
name: 'nextAuthSecret',
type: 'text',
message: `What is the NextAuth.js Secret? (Hit enter to use a randomly generated secret)`,
initial:
process.env.NEXTAUTH_SECRET ||
crypto.lib.WordArray.random(16).toString(),
initial: process.env.NEXTAUTH_SECRET || randomBytes(16).toString('hex'),
},
]);
config.envVars.push({
Expand Down
1 change: 0 additions & 1 deletion packages/@tinacms/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"scripts": {
"types": "pnpm tsc",
"build": "tinacms-scripts build",
"docs": "pnpm typedoc",
"test": "vitest run --coverage.enabled",
"test-watch": "vitest"
},
Expand Down
9 changes: 0 additions & 9 deletions packages/@tinacms/graphql/typedoc.json

This file was deleted.

Loading
Loading