From 02c05cbd0bb0d082a5a725965c91ccd899e05c79 Mon Sep 17 00:00:00 2001 From: Dawson Toth Date: Fri, 10 Jul 2026 16:19:18 -0400 Subject: [PATCH] fix(applications): preserve in-table comments in the visual schema editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The visual schema editor keeps untouched tables byte-for-byte but regenerates a table from its parsed model the moment it's edited. The model had nowhere to store comments that live inside a `@table` block but aren't attached above a field, so editing a table silently dropped or relocated them. Because unedited files round-trip perfectly, the loss only surfaced on the first edit — an inconsistent state (studio #1462). Three failure modes, all now covered by regression tests: - A comment trailing the last field (before `}`) was accumulated as pending trivia and discarded when no field followed. `parseFields` now returns the leftover as `trailingComments`; `generateTable` re-emits it before `}`. - A comments-only body dropped the comment AND emitted `type X @table {\n}` — an empty field block, which is a hard graphql-js syntax error that makes Harper reject the whole schema (the "table never loads" symptom). To keep that state unreachable, the visual editor now blocks removing a table's last field and flags a zero-field table with a warning. - A header-line comment (`type X @table { # note`) was relocated to a standalone line above the first field. It's now captured as `headerComment` and re-emitted on the header line. Co-Authored-By: Claude Opus 4.8 --- .../components/SchemaEditorView/FieldRow.tsx | 7 ++- .../components/SchemaEditorView/TableCard.tsx | 7 +++ .../applications/lib/schema/mutations.ts | 1 + .../lib/schema/parseSchema.test.ts | 26 +++++++++++ .../applications/lib/schema/parseSchema.ts | 46 ++++++++++++++++--- .../lib/schema/serializeSchema.test.ts | 34 ++++++++++++++ .../lib/schema/serializeSchema.ts | 30 ++++++++---- .../instance/applications/lib/schema/types.ts | 13 ++++++ 8 files changed, 147 insertions(+), 17 deletions(-) diff --git a/src/features/instance/applications/components/SchemaEditorView/FieldRow.tsx b/src/features/instance/applications/components/SchemaEditorView/FieldRow.tsx index 5a31b374e..0c52a97d5 100644 --- a/src/features/instance/applications/components/SchemaEditorView/FieldRow.tsx +++ b/src/features/instance/applications/components/SchemaEditorView/FieldRow.tsx @@ -41,12 +41,15 @@ export function FieldRow({ field, typeNames, readOnly, + disableRemove, onChange, onRemove, }: { field: FieldModel; typeNames: string[]; readOnly: boolean; + /** True when this is the table's only field: a GraphQL type needs at least one, so removal is blocked. */ + disableRemove?: boolean; onChange: (field: FieldModel) => void; onRemove: () => void; }) { @@ -107,9 +110,9 @@ export function FieldRow({ variant="destructiveGhost" size="icon" className="self-end" - disabled={readOnly} + disabled={readOnly || disableRemove} onClick={onRemove} - title="Remove field" + title={disableRemove ? 'A table needs at least one field' : 'Remove field'} > diff --git a/src/features/instance/applications/components/SchemaEditorView/TableCard.tsx b/src/features/instance/applications/components/SchemaEditorView/TableCard.tsx index bed3a4fe3..aa89fefd7 100644 --- a/src/features/instance/applications/components/SchemaEditorView/TableCard.tsx +++ b/src/features/instance/applications/components/SchemaEditorView/TableCard.tsx @@ -89,6 +89,7 @@ export function TableCard({ {fieldCount} field{fieldCount === 1 ? '' : 's'} {exported ? ' · REST' : ''} {nameError ? ' · ⚠ invalid name' : ''} + {fieldCount === 0 ? ' · ⚠ needs a field' : ''}