From e2b3dcd64075ac6aaca33f6b9cfcc8f234822a5f Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:54:22 +0200 Subject: [PATCH 1/9] Return canonical records after document duplication --- includes/Rest/DocumentsController.php | 11 ++- tests/php/test-rest-collections.php | 97 +++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/includes/Rest/DocumentsController.php b/includes/Rest/DocumentsController.php index 814148a4..2a21bd83 100644 --- a/includes/Rest/DocumentsController.php +++ b/includes/Rest/DocumentsController.php @@ -346,6 +346,11 @@ public function duplicate( WP_REST_Request $request ): WP_REST_Response|WP_Error return $result; } + $post = get_post( (int) $result['id'] ); + $result['post'] = $post instanceof WP_Post + ? $this->prepared_post( $post ) + : null; + return new WP_REST_Response( $result, 201 ); } @@ -580,9 +585,9 @@ private static function content_depends_on_collection( string $content, int $col } /** - * Runs the standard `WP_REST_Posts_Controller` against the given document - * so the response payload matches what `useEntityRecord` already knows how - * to consume. Lets clients drop a follow-up GET after a successful restore. + * Returns the document in the REST post shape expected by `useEntityRecord`. + * Restore and duplicate responses can include it instead of requiring + * another GET. * * @param WP_Post $post Document post to render. * diff --git a/tests/php/test-rest-collections.php b/tests/php/test-rest-collections.php index ea08c160..f31b1973 100644 --- a/tests/php/test-rest-collections.php +++ b/tests/php/test-rest-collections.php @@ -24,13 +24,16 @@ use Cortext\PostType\DocumentIdentity; use Cortext\PostType\Field; use Cortext\Rest\DocumentsController; +use Cortext\Rest\RowsController; use Cortext\Taxonomy\TraitTaxonomy; use WorDBless\BaseTestCase; +use WP_Error; use WP_REST_Request; use WP_REST_Server; final class Test_Rest_Collections extends BaseTestCase { + use InMemoryPostsQuery; use InMemoryTermStore; public function set_up(): void { @@ -49,13 +52,16 @@ public function set_up(): void { ( new Document() )->register_collection_meta(); $this->install_in_memory_term_store(); + $this->install_in_memory_posts_query(); $GLOBALS['wp_rest_server'] = new WP_REST_Server(); ( new DocumentsController() )->register(); + ( new RowsController() )->register(); do_action( 'rest_api_init' ); } public function tear_down(): void { + $this->uninstall_in_memory_posts_query(); $this->uninstall_in_memory_term_store(); wp_set_current_user( 0 ); @@ -182,9 +188,16 @@ public function test_duplicate_clones_schema_and_owner(): void { $this->assertSame( 201, $response->get_status() ); $data = $response->get_data(); + $this->assertSame( + array( 'id', 'title', 'slug', 'restBase', 'parent', 'collection_id', 'skipped_fields', 'post' ), + array_keys( $data ) + ); $this->assertSame( 'Copy of Quarterly reports', $data['title'] ); $this->assertSame( $page_id, $data['parent'] ); $this->assertSame( array(), $data['skipped_fields'] ); + $this->assertIsArray( $data['post'] ); + $this->assertSame( $data['id'], $data['post']['id'] ); + $this->assertSame( 'Copy of Quarterly reports', $data['post']['title']['raw'] ); $new_field_ids = $this->stored_collection_field_ids( (int) $data['id'] ); // The source's seeded "Title" placeholder is cloned alongside the @@ -268,6 +281,73 @@ public function test_duplicate_remaps_rollup_references_to_cloned_field_ids(): v ); } + public function test_duplicate_succeeds_when_canonical_post_cannot_be_prepared(): void { + wp_set_current_user( $this->create_user( 'administrator' ) ); + $source = $this->create_page(); + + $fail_core_read = static function ( $response, $_handler, WP_REST_Request $request ) { + if ( + 'GET' === $request->get_method() + && str_starts_with( $request->get_route(), '/wp/v2/crtxt_documents/' ) + ) { + return new WP_Error( 'cortext_test_prepared_post_failed', 'Forced preparation failure.' ); + } + return $response; + }; + add_filter( 'rest_request_before_callbacks', $fail_core_read, 10, 3 ); + try { + $response = $this->duplicate_collection( $source ); + } finally { + remove_filter( 'rest_request_before_callbacks', $fail_core_read, 10 ); + } + + $this->assertSame( 201, $response->get_status() ); + $data = $response->get_data(); + $this->assertGreaterThan( 0, $data['id'] ); + $this->assertNull( $data['post'] ); + $this->assertInstanceOf( \WP_Post::class, get_post( $data['id'] ) ); + } + + public function test_duplicate_row_returns_canonical_post_with_copied_hydrated_values(): void { + wp_set_current_user( $this->create_user( 'administrator' ) ); + + $collection_id = $this->create_collection( 'invoices', 'Invoices' ); + $amount_id = $this->attach_scalar_field( $collection_id, 'Amount', 'number' ); + $tags_id = $this->attach_scalar_field( $collection_id, 'Tags', 'multiselect' ); + $row_id = $this->create_row( $collection_id, 'July invoice' ); + + update_post_meta( $row_id, 'field-' . $amount_id, '7.5' ); + add_post_meta( $row_id, 'field-' . $tags_id, 'Paid' ); + add_post_meta( $row_id, 'field-' . $tags_id, 'Priority' ); + + // Production init registers collection fields as document meta. Do the + // same here before dispatching the REST request. + ( new Document() )->register_field_meta(); + + $response = $this->duplicate_collection( $row_id ); + + $this->assertSame( 201, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( $collection_id, $data['collection_id'] ); + $this->assertIsArray( $data['post'] ); + $this->assertSame( $data['id'], $data['post']['id'] ); + $this->assertSame( 'Copy of July invoice', $data['post']['title']['raw'] ); + $this->assertSame( 'private', $data['post']['status'] ); + $this->assertSame( 7.5, $data['post']['meta'][ 'field-' . $amount_id ] ); + $this->assertSame( + array( 'Paid', 'Priority' ), + $data['post']['meta'][ 'field-' . $tags_id ] + ); + $this->assertSame( + 7.5, + $data['post']['cortext_hydrated_meta'][ 'field-' . $amount_id ] + ); + $this->assertSame( + array( 'Paid', 'Priority' ), + $data['post']['cortext_hydrated_meta'][ 'field-' . $tags_id ] + ); + } + public function test_duplicate_returns_404_for_unknown_id(): void { wp_set_current_user( $this->create_user( 'administrator' ) ); @@ -319,6 +399,23 @@ private function attach_scalar_field( int $collection_id, string $title, string return $field_id; } + private function create_row( int $collection_id, string $title ): int { + $id = (int) wp_insert_post( + array( + 'post_type' => Document::POST_TYPE, + 'post_status' => 'private', + 'post_title' => $title, + ) + ); + $this->assertGreaterThan( 0, $id ); + + $term_id = TraitTaxonomy::term_id_for_trait( $collection_id ); + $this->assertGreaterThan( 0, $term_id ); + wp_set_object_terms( $id, array( $term_id ), TraitTaxonomy::TAXONOMY, false ); + + return $id; + } + private function create_user( string $role ): int { return (int) wp_insert_user( array( From c4f3d2f072c71226bed0ac48c9c43dab972b255c Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:55:04 +0200 Subject: [PATCH 2/9] Save row fields through core-data --- src/components/CollectionDataViews.js | 12 ++- src/components/RowProperties.js | 17 ++-- src/components/rowDocumentMutations.js | 24 ++++-- tests/js/components/RowProperties.test.js | 81 +++++++++++-------- .../components/rowDocumentMutations.test.js | 52 ++++++++---- 5 files changed, 120 insertions(+), 66 deletions(-) diff --git a/src/components/CollectionDataViews.js b/src/components/CollectionDataViews.js index 68df4673..985ee072 100644 --- a/src/components/CollectionDataViews.js +++ b/src/components/CollectionDataViews.js @@ -1,5 +1,7 @@ import apiFetch from '@wordpress/api-fetch'; import { Notice } from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; import { DataViews } from '@wordpress/dataviews/wp'; import { useCallback, @@ -109,6 +111,7 @@ export default function CollectionDataViews( { } ) { const { fields, collection, isResolving, fieldsResolved } = useCollectionFieldsContext(); + const { saveEntityRecord } = useDispatch( coreStore ); const { touchRecent } = useRecents(); // Field IDs from the last schema sync. We use this to auto-show fields // the user just created. `null` on first run means the saved view should @@ -557,7 +560,12 @@ export default function CollectionDataViews( { if ( ! collectionId || ! rowId ) { return null; } - const updated = await saveRowDocumentField( rowId, fieldId, value ); + const updated = await saveRowDocumentField( + saveEntityRecord, + rowId, + fieldId, + value + ); touchRecent( { kind: 'row', id: updated?.id ?? rowId, @@ -567,7 +575,7 @@ export default function CollectionDataViews( { notifyCollectionRowsChanged( collectionId ); return updated; }, - [ collectionId, refresh, touchRecent ] + [ collectionId, refresh, saveEntityRecord, touchRecent ] ); let dataViewLayoutType = 'table'; diff --git a/src/components/RowProperties.js b/src/components/RowProperties.js index 21fa952f..f8eebb05 100644 --- a/src/components/RowProperties.js +++ b/src/components/RowProperties.js @@ -11,7 +11,8 @@ */ import { Button, CheckboxControl } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; import { Fragment, @@ -1035,6 +1036,7 @@ export default function RowProperties( { updateFieldFormat, refreshRows, } = useContext( RowMutationContext ); + const { saveEntityRecord } = useDispatch( coreStore ); const [ localOptionOverrides, setLocalOptionOverrides ] = useState( {} ); const [ localFormatOverrides, setLocalFormatOverrides ] = useState( {} ); const [ activeLayoutFieldId, setActiveLayoutFieldId ] = useState( null ); @@ -1075,12 +1077,9 @@ export default function RowProperties( { }, [ updateFieldFormat ] ); - // Field edits save as minimal REST patches (see `rowDocumentMutations`) - // while the panel still reads from `editorStore`, so this hand-rolled - // optimistic layer bridges the two: `local*` holds the in-flight edit and - // `committed*` holds a saved value until the live `row` catches up. Rows in - // core-data (tech-debt.md#td-rows-not-in-core-data) would let - // `saveEntityRecord` do this and delete the block. + // Row writes use core-data, but this panel still reads from `editorStore` + // and the REST row. Keep in-flight (`local*`) and saved (`committed*`) values + // here until row reads move too. const [ committedMeta, setCommittedMeta ] = useState( {} ); const [ committedHydratedMeta, setCommittedHydratedMeta ] = useState( {} ); const [ committedTitle, setCommittedTitle ] = useState( undefined ); @@ -1192,6 +1191,7 @@ export default function RowProperties( { try { const updated = await saveRowDocumentField( + saveEntityRecord, entry.rowId, fieldId, entry.value @@ -1237,6 +1237,7 @@ export default function RowProperties( { clearSaveTimer, collectionId, refreshRows, + saveEntityRecord, ] ); @@ -1289,6 +1290,7 @@ export default function RowProperties( { for ( const [ fieldId, entry ] of entries ) { try { const updated = await saveRowDocumentField( + saveEntityRecord, entry.rowId, fieldId, entry.value @@ -1329,6 +1331,7 @@ export default function RowProperties( { clearLocalFieldValue, collectionId, refreshRows, + saveEntityRecord, ] ); diff --git a/src/components/rowDocumentMutations.js b/src/components/rowDocumentMutations.js index 13aef2e1..cda6f226 100644 --- a/src/components/rowDocumentMutations.js +++ b/src/components/rowDocumentMutations.js @@ -1,5 +1,4 @@ -import apiFetch from '@wordpress/api-fetch'; - +import { DOCUMENT_POST_TYPE } from '../collections'; import { TITLE_FIELD_ID } from './dataViewColumns'; export function rowDocumentFieldPayload( fieldId, value ) { @@ -10,10 +9,19 @@ export function rowDocumentFieldPayload( fieldId, value ) { return { meta: { [ fieldId ]: value } }; } -export function saveRowDocumentField( rowId, fieldId, value ) { - return apiFetch( { - path: `/wp/v2/crtxt_documents/${ rowId }`, - method: 'POST', - data: rowDocumentFieldPayload( fieldId, value ), - } ); +export function saveRowDocumentField( + saveEntityRecord, + rowId, + fieldId, + value +) { + return saveEntityRecord( + 'postType', + DOCUMENT_POST_TYPE, + { + id: rowId, + ...rowDocumentFieldPayload( fieldId, value ), + }, + { throwOnError: true } + ); } diff --git a/tests/js/components/RowProperties.test.js b/tests/js/components/RowProperties.test.js index 02c1556a..a30fad2e 100644 --- a/tests/js/components/RowProperties.test.js +++ b/tests/js/components/RowProperties.test.js @@ -11,9 +11,9 @@ let mockDndProps; let mockSortableContextProps; const mockEditPost = jest.fn(); +const mockSaveEntityRecord = jest.fn(); const mockRelationEditorProps = []; -jest.mock( '@wordpress/api-fetch', () => jest.fn() ); jest.mock( '@wordpress/components', () => { const { createElement, forwardRef } = require( '@wordpress/element' ); @@ -68,6 +68,10 @@ jest.mock( '@wordpress/data', () => ( { useSelect: jest.fn(), } ) ); +jest.mock( '@wordpress/core-data', () => ( { + store: { name: 'core' }, +} ) ); + jest.mock( '@wordpress/editor', () => ( { store: 'editor-store', } ) ); @@ -182,7 +186,6 @@ jest.mock( '../../../src/components/relations/RelationEditor', () => ( { }, } ) ); -import apiFetch from '@wordpress/api-fetch'; import { useDispatch, useSelect } from '@wordpress/data'; import { closestCenter, pointerWithin, useDroppable } from '@dnd-kit/core'; import { useSortable } from '@dnd-kit/sortable'; @@ -196,7 +199,7 @@ describe( 'RowProperties', () => { beforeEach( () => { mockDndProps = null; mockSortableContextProps = null; - apiFetch.mockReset(); + mockSaveEntityRecord.mockReset(); closestCenter.mockReset(); pointerWithin.mockReset(); useDroppable.mockReturnValue( { @@ -213,7 +216,10 @@ describe( 'RowProperties', () => { } ); mockEditPost.mockReset(); mockRelationEditorProps.length = 0; - useDispatch.mockReturnValue( { editPost: mockEditPost } ); + useDispatch.mockReturnValue( { + editPost: mockEditPost, + saveEntityRecord: mockSaveEntityRecord, + } ); useSelect.mockReturnValue( { title: 'Current title', meta: { 'field-7': 'Open' }, @@ -351,9 +357,9 @@ describe( 'RowProperties', () => { expect( handle ).not.toHaveFocus(); } ); - it( 'keeps text-like properties single-line while saving a minimal row patch', async () => { + it( 'keeps text-like properties on one line and saves only the changed field', async () => { jest.useFakeTimers(); - apiFetch.mockResolvedValue( { + mockSaveEntityRecord.mockResolvedValue( { id: 99, title: { raw: 'Current title', rendered: 'Current title' }, meta: { 'field-7': 'https://example.com bad' }, @@ -388,7 +394,7 @@ describe( 'RowProperties', () => { } ); expect( input ).toHaveValue( 'https://example.com bad' ); - expect( apiFetch ).not.toHaveBeenCalled(); + expect( mockSaveEntityRecord ).not.toHaveBeenCalled(); await act( async () => { jest.advanceTimersByTime( 500 ); @@ -396,13 +402,15 @@ describe( 'RowProperties', () => { } ); await waitFor( () => - expect( apiFetch ).toHaveBeenCalledWith( { - path: '/wp/v2/crtxt_documents/99', - method: 'POST', - data: { + expect( mockSaveEntityRecord ).toHaveBeenCalledWith( + 'postType', + 'crtxt_document', + { + id: 99, meta: { 'field-7': 'https://example.com bad' }, }, - } ) + { throwOnError: true } + ) ); expect( mockEditPost ).not.toHaveBeenCalled(); } finally { @@ -443,9 +451,9 @@ describe( 'RowProperties', () => { ); } ); - it( 'keeps a saved property visible until the row fallback catches up', async () => { + it( 'keeps a saved value visible while the row fallback catches up', async () => { jest.useFakeTimers(); - apiFetch.mockResolvedValue( { + mockSaveEntityRecord.mockResolvedValue( { id: 99, meta: { 'field-7': 'Doing' }, } ); @@ -519,7 +527,7 @@ describe( 'RowProperties', () => { it( 'keeps field save responses from overwriting other edited fields', async () => { jest.useFakeTimers(); const resolvers = []; - apiFetch.mockImplementation( + mockSaveEntityRecord.mockImplementation( () => new Promise( ( resolve ) => { resolvers.push( resolve ); @@ -569,16 +577,20 @@ describe( 'RowProperties', () => { await Promise.resolve(); } ); - expect( apiFetch ).toHaveBeenNthCalledWith( 1, { - path: '/wp/v2/crtxt_documents/99', - method: 'POST', - data: { meta: { 'field-7': 'Doing' } }, - } ); - expect( apiFetch ).toHaveBeenNthCalledWith( 2, { - path: '/wp/v2/crtxt_documents/99', - method: 'POST', - data: { meta: { 'field-8': 'Tagged' } }, - } ); + expect( mockSaveEntityRecord ).toHaveBeenNthCalledWith( + 1, + 'postType', + 'crtxt_document', + { id: 99, meta: { 'field-7': 'Doing' } }, + { throwOnError: true } + ); + expect( mockSaveEntityRecord ).toHaveBeenNthCalledWith( + 2, + 'postType', + 'crtxt_document', + { id: 99, meta: { 'field-8': 'Tagged' } }, + { throwOnError: true } + ); await act( async () => { resolvers[ 1 ]( { @@ -820,14 +832,15 @@ describe( 'RowProperties', () => { ).not.toBeInTheDocument(); } ); - it( 'saves relation edits through the row endpoint and updates the displayed chip', async () => { + it( 'saves relation edits through core-data and updates the chip from the hydrated response', async () => { const refreshRows = jest.fn(); const onRowsChanged = jest.fn(); window.addEventListener( COLLECTION_ROWS_CHANGED_EVENT, onRowsChanged ); - apiFetch.mockResolvedValue( { + mockSaveEntityRecord.mockResolvedValue( { id: 99, title: { raw: 'Source row', rendered: 'Source row' }, - meta: { + meta: { 'field-7': [ 456 ] }, + cortext_hydrated_meta: { 'field-7': [ { id: 456, @@ -883,13 +896,15 @@ describe( 'RowProperties', () => { ); await waitFor( () => - expect( apiFetch ).toHaveBeenCalledWith( { - path: '/wp/v2/crtxt_documents/99', - method: 'POST', - data: { + expect( mockSaveEntityRecord ).toHaveBeenCalledWith( + 'postType', + 'crtxt_document', + { + id: 99, meta: { 'field-7': [ 456 ] }, }, - } ) + { throwOnError: true } + ) ); await screen.findByRole( 'button', { name: 'Grace Hopper' } ); expect( refreshRows ).toHaveBeenCalled(); diff --git a/tests/js/components/rowDocumentMutations.test.js b/tests/js/components/rowDocumentMutations.test.js index 9b61429a..2ce001c9 100644 --- a/tests/js/components/rowDocumentMutations.test.js +++ b/tests/js/components/rowDocumentMutations.test.js @@ -1,32 +1,52 @@ -import apiFetch from '@wordpress/api-fetch'; - import { rowDocumentFieldPayload, saveRowDocumentField, } from '../../../src/components/rowDocumentMutations'; - -jest.mock( '@wordpress/api-fetch', () => jest.fn() ); +import { DOCUMENT_POST_TYPE } from '../../../src/collections'; describe( 'rowDocumentMutations', () => { - beforeEach( () => { - apiFetch.mockReset(); - } ); - it( 'saves title as a top-level document attribute', async () => { - apiFetch.mockResolvedValue( { id: 9 } ); + const saveEntityRecord = jest.fn().mockResolvedValue( { id: 9 } ); - await saveRowDocumentField( 9, 'title', 'New title' ); + const saved = await saveRowDocumentField( + saveEntityRecord, + 9, + 'title', + 'New title' + ); - expect( apiFetch ).toHaveBeenCalledWith( { - path: '/wp/v2/crtxt_documents/9', - method: 'POST', - data: { title: 'New title' }, - } ); + expect( saveEntityRecord ).toHaveBeenCalledWith( + 'postType', + DOCUMENT_POST_TYPE, + { id: 9, title: 'New title' }, + { throwOnError: true } + ); + expect( saved ).toEqual( { id: 9 } ); } ); - it( 'saves collection fields as meta patches', () => { + it( 'saves only the changed collection field in meta', async () => { + const saveEntityRecord = jest.fn().mockResolvedValue( { id: 9 } ); + expect( rowDocumentFieldPayload( 'field-7', 'Open' ) ).toEqual( { meta: { 'field-7': 'Open' }, } ); + + await saveRowDocumentField( saveEntityRecord, 9, 'field-7', 'Open' ); + + expect( saveEntityRecord ).toHaveBeenCalledWith( + 'postType', + DOCUMENT_POST_TYPE, + { id: 9, meta: { 'field-7': 'Open' } }, + { throwOnError: true } + ); + } ); + + it( 'rejects with the original error when saving a field fails', async () => { + const error = new Error( 'Save failed' ); + const saveEntityRecord = jest.fn().mockRejectedValue( error ); + + await expect( + saveRowDocumentField( saveEntityRecord, 9, 'field-7', 'Blocked' ) + ).rejects.toBe( error ); } ); } ); From bede8737dd7e96c241d56b4e476722715f905f5b Mon Sep 17 00:00:00 2001 From: priethor <27339341+priethor@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:55:35 +0200 Subject: [PATCH 3/9] Create rows through core-data --- src/components/DataViewNewRowButton.js | 24 ++--- src/components/relations/RelationEditor.js | 14 +-- src/components/rowDocumentCreation.js | 44 +++++++++ src/documents/actions.js | 3 +- .../components/DataViewNewRowButton.test.js | 92 +++++++++++++++++ .../relations/RelationEditor.test.js | 60 +++++++++--- .../js/components/rowDocumentCreation.test.js | 98 +++++++++++++++++++ tests/js/documents/actions.test.js | 28 ++++-- 8 files changed, 324 insertions(+), 39 deletions(-) create mode 100644 src/components/rowDocumentCreation.js create mode 100644 tests/js/components/DataViewNewRowButton.test.js create mode 100644 tests/js/components/rowDocumentCreation.test.js diff --git a/src/components/DataViewNewRowButton.js b/src/components/DataViewNewRowButton.js index 5cc35e8a..294605cb 100644 --- a/src/components/DataViewNewRowButton.js +++ b/src/components/DataViewNewRowButton.js @@ -1,9 +1,10 @@ -import apiFetch from '@wordpress/api-fetch'; import { Button, Notice } from '@wordpress/components'; import { useCallback, useMemo, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { plus } from '@wordpress/icons'; +import { useCreateRowDocument } from './rowDocumentCreation'; + // Pull a simple `is` prefill from the active filters. Multi-value operators // are ignored for now; this path only handles one scalar value per field. // Filters already run on GET /cortext/v1/rows, so prefill is only a convenience @@ -46,6 +47,7 @@ export default function DataViewNewRowButton( { } ) { const [ isCreating, setIsCreating ] = useState( false ); const [ error, setError ] = useState( null ); + const createRowDocument = useCreateRowDocument(); const prefillableFieldIds = useMemo( () => @@ -65,15 +67,9 @@ export default function DataViewNewRowButton( { setError( null ); const meta = prefillFromFilters( view?.filters, prefillableFieldIds ); try { - const created = await apiFetch( { - path: '/wp/v2/crtxt_documents', - method: 'POST', - data: { - status: 'private', - title: '', - cortext_trait: collectionId, - ...( Object.keys( meta ).length ? { meta } : {} ), - }, + const created = await createRowDocument( { + collectionId, + meta, } ); onCreated( created ); } catch ( err ) { @@ -83,7 +79,13 @@ export default function DataViewNewRowButton( { } finally { setIsCreating( false ); } - }, [ collectionId, view, prefillableFieldIds, onCreated ] ); + }, [ + collectionId, + view, + prefillableFieldIds, + onCreated, + createRowDocument, + ] ); const button = (