From 2d209cc8a5884c92eb61a2f995dcd7967f06fe13 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 08:45:00 +0000 Subject: [PATCH 1/4] fix(plugin-form): a create form preselects the option a field marks default: true A field that declares no `defaultValue` but marks one option `default: true` opened on the "Select an option" placeholder, while a create that omitted the field stored the marked option: the server's insert path falls back to it since objectstack#7246. `schemaDefaultValues`, the one function every create-form container seeds through, now reads the option list the same way the engine's `resolveOptionDefault` does: only when `defaultValue` is absent (== null), first marked option on a single-valued field, every marked option as an array on a multi-valued one (`isMultiValueField`). Edit forms and caller seeds are unchanged. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude --- .../optionDefaultSeed-11914.test.tsx | 327 ++++++++++++++++++ packages/plugin-form/src/schemaDefaults.ts | 123 ++++++- 2 files changed, 433 insertions(+), 17 deletions(-) create mode 100644 packages/plugin-form/src/__tests__/optionDefaultSeed-11914.test.tsx diff --git a/packages/plugin-form/src/__tests__/optionDefaultSeed-11914.test.tsx b/packages/plugin-form/src/__tests__/optionDefaultSeed-11914.test.tsx new file mode 100644 index 0000000000..b9125f44b5 --- /dev/null +++ b/packages/plugin-form/src/__tests__/optionDefaultSeed-11914.test.tsx @@ -0,0 +1,327 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#11914 — a CREATE form preselects the option a field's option list + * marks `default: true`. + * + * Measured before the fix through the console's create dialog (the global + * `ModalForm`) on the objectstack tutorial's verbatim ticket object: Priority + * and Status, both required `select`s that mark one option `default: true` and + * declare no field-level `defaultValue`, opened on the "Select an option" + * placeholder. The form could not be submitted until the user picked the very + * values the metadata had already chosen — while the server, given the same + * create without those keys, stores the marked options. + * + * The server's rule is `ObjectQL.applyFieldDefaults` falling back to its + * `resolveOptionDefault` (objectstack#7246) when the field declares no + * `defaultValue`. The pins below are that rule, read from the client side: + * + * - one marked option is preselected, and submitted without a manual pick; + * - CONTROL: a field whose options mark nothing stays empty; + * - a field-level `defaultValue` wins over the option flag — including a + * runtime one, which the server resolves at insert and the form leaves + * empty, so the flag is never seeded in its place; + * - a single-valued field takes the FIRST marked option; a multi-valued one + * takes every marked option, as an array; + * - an explicit choice wins: the caller's seed, and the user's own pick; + * - an EDIT form never applies it — the stored record is what it shows. + */ + +import React from 'react'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, fireEvent, waitFor, cleanup } from '@testing-library/react'; +import { registerAllFields } from '@object-ui/fields'; +import { schemaDefaultValues, seedCreateValues } from '../schemaDefaults'; +import { ModalForm } from '../ModalForm'; +import { ObjectForm } from '../ObjectForm'; + +registerAllFields(); + +/** + * The objectstack tutorial's ticket object (`build-with-claude-code.mdx`, + * step 3), as the server serves it: `Field.select` returns its config + * unchanged, so neither field carries a `defaultValue`. + */ +const TICKET = { + name: 'support_desk_ticket', + fields: { + subject: { type: 'text', label: 'Subject', required: true }, + priority: { + type: 'select', + label: 'Priority', + required: true, + options: [ + { label: 'Low', value: 'low', default: true }, + { label: 'Normal', value: 'normal' }, + { label: 'High', value: 'high' }, + { label: 'Urgent', value: 'urgent' }, + ], + }, + status: { + type: 'select', + label: 'Status', + required: true, + options: [ + { label: 'Open', value: 'open', color: '#3B82F6', default: true }, + { label: 'Pending', value: 'pending', color: '#F59E0B' }, + { label: 'Resolved', value: 'resolved', color: '#10B981' }, + { label: 'Closed', value: 'closed', color: '#6B7280' }, + ], + }, + // CONTROL: options that mark nothing. + channel: { + type: 'select', + label: 'Channel', + options: [ + { label: 'Email', value: 'email' }, + { label: 'Phone', value: 'phone' }, + ], + }, + }, +}; + +const makeDS = (objectSchema: unknown = TICKET, record?: unknown) => + ({ + getObjectSchema: vi.fn().mockResolvedValue(objectSchema), + create: vi.fn().mockResolvedValue({ id: 't1' }), + update: vi.fn().mockResolvedValue({ id: 't1' }), + findOne: vi.fn().mockResolvedValue(record ?? { id: 't1' }), + }) as any; + +const triggerText = (field: string) => + document.body.querySelector(`[data-testid="select-trigger-${field}"]`)?.textContent ?? ''; + +/** Radix renders a hidden native `