diff --git a/apps/nestjs-backend/src/features/field/open-api/field-open-api-v2.service.ts b/apps/nestjs-backend/src/features/field/open-api/field-open-api-v2.service.ts index 9fec2935ec..29105d54ec 100644 --- a/apps/nestjs-backend/src/features/field/open-api/field-open-api-v2.service.ts +++ b/apps/nestjs-backend/src/features/field/open-api/field-open-api-v2.service.ts @@ -1271,6 +1271,10 @@ export class FieldOpenApiV2Service { context ); const { hasAiConfig, nextAiConfig, v2Field } = preparedField; + const legacyViewId = + fieldRo && typeof fieldRo === 'object' && 'viewId' in fieldRo + ? (fieldRo.viewId as string | undefined) + : undefined; const legacyOrder = fieldRo && typeof fieldRo === 'object' && 'order' in fieldRo ? (fieldRo.order as @@ -1291,6 +1295,7 @@ export class FieldOpenApiV2Service { baseId: table.baseId().toString(), tableId, field: v2Field, + ...(typeof legacyViewId === 'string' ? { viewId: legacyViewId } : {}), ...(normalizedOrder ? { order: normalizedOrder } : {}), }); diff --git a/apps/nextjs-app/src/features/app/components/field-setting/FieldSetting.tsx b/apps/nextjs-app/src/features/app/components/field-setting/FieldSetting.tsx index 6543196a2a..ab68c682df 100644 --- a/apps/nextjs-app/src/features/app/components/field-setting/FieldSetting.tsx +++ b/apps/nextjs-app/src/features/app/components/field-setting/FieldSetting.tsx @@ -211,7 +211,10 @@ export const FieldSetting = (props: IFieldSetting) => { let result: IFieldVo | undefined; try { if (operator === FieldOperator.Add) { - result = await createNewField(field); + result = await createNewField({ + ...field, + viewId: view?.id, + }); } if (operator === FieldOperator.Insert) { diff --git a/packages/core/src/models/field/field.schema.ts b/packages/core/src/models/field/field.schema.ts index 1209a32065..7cadeb9279 100644 --- a/packages/core/src/models/field/field.schema.ts +++ b/packages/core/src/models/field/field.schema.ts @@ -329,6 +329,10 @@ export const createFieldRoSchema = baseFieldRoSchema 'The id of the field that start with "fld", followed by exactly 16 alphanumeric characters `/^fld[\\da-zA-Z]{16}$/`. It is sometimes useful to specify an id at creation time', example: 'fldxxxxxxxxxxxxxxxx', }), + viewId: z.string().startsWith(IdPrefix.View).optional().meta({ + description: + 'The id of the current view where the field is being created. Used to prevent auto-hiding the new field in this view.', + }), order: z .object({ viewId: z.string().meta({ diff --git a/packages/v2/core/src/commands/CreateFieldCommand.ts b/packages/v2/core/src/commands/CreateFieldCommand.ts index 3893609f94..84d81a42c8 100644 --- a/packages/v2/core/src/commands/CreateFieldCommand.ts +++ b/packages/v2/core/src/commands/CreateFieldCommand.ts @@ -14,6 +14,7 @@ export const createFieldInputSchema = z.object({ baseId: z.string(), tableId: z.string(), field: tableFieldInputSchema, + viewId: z.string().optional(), order: z .object({ viewId: z.string(), @@ -29,6 +30,7 @@ export class CreateFieldCommand extends TableUpdateCommand { readonly baseId: BaseId, readonly tableId: TableId, readonly field: z.output, + readonly viewId?: string, readonly order?: { viewId: string; orderIndex: number; @@ -57,7 +59,8 @@ export class CreateFieldCommand extends TableUpdateCommand { return BaseId.create(parsed.data.baseId).andThen((baseId) => TableId.create(parsed.data.tableId).map( - (tableId) => new CreateFieldCommand(baseId, tableId, parsed.data.field, parsed.data.order) + (tableId) => + new CreateFieldCommand(baseId, tableId, parsed.data.field, parsed.data.viewId, parsed.data.order) ) ); } diff --git a/packages/v2/core/src/commands/CreateFieldHandler.ts b/packages/v2/core/src/commands/CreateFieldHandler.ts index ab0bda6fa4..5ba23eb005 100644 --- a/packages/v2/core/src/commands/CreateFieldHandler.ts +++ b/packages/v2/core/src/commands/CreateFieldHandler.ts @@ -143,6 +143,14 @@ export class CreateFieldHandler implements ICommandHandler + table.update((mutator) => + mutator.addField(field, { ...addFieldOptions, targetViewId: viewId }) + ) + ); + } return table.update((mutator) => mutator.addField(field, addFieldOptions)); } diff --git a/packages/v2/core/src/domain/table/Table.spec.ts b/packages/v2/core/src/domain/table/Table.spec.ts index 296d909b33..c6506e93fb 100644 --- a/packages/v2/core/src/domain/table/Table.spec.ts +++ b/packages/v2/core/src/domain/table/Table.spec.ts @@ -516,6 +516,89 @@ describe('Table', () => { expect(targetEntry?.order).toBe(1.5); }); + it('keeps newly added field visible in the target view without explicit ordering', () => { + const newFieldId = createFieldId('k')._unsafeUnwrap(); + const builder = Table.builder() + .withBaseId(createBaseId('k')._unsafeUnwrap()) + .withName(TableName.create('Add Without Order')._unsafeUnwrap()); + + builder.field().singleLineText().withName(FieldName.create('Title')._unsafeUnwrap()).done(); + builder.field().singleLineText().withName(FieldName.create('Notes')._unsafeUnwrap()).done(); + builder.view().grid().withName(ViewName.create('View A')._unsafeUnwrap()).done(); + builder.view().grid().withName(ViewName.create('View B')._unsafeUnwrap()).done(); + + const table = builder.build()._unsafeUnwrap(); + const notesField = table.getFields().find((field) => field.name().toString() === 'Notes'); + const targetView = table.views()[0]; + const otherView = table.views()[1]; + + expect(notesField).toBeTruthy(); + expect(targetView).toBeTruthy(); + expect(otherView).toBeTruthy(); + if (!notesField || !targetView || !otherView) return; + + // Configure both views with explicit hidden visibility config + const targetViewMeta = targetView.columnMeta()._unsafeUnwrap().toDto(); + const otherViewMeta = otherView.columnMeta()._unsafeUnwrap().toDto(); + + const configuredTargetMeta = ViewColumnMeta.create({ + ...targetViewMeta, + [notesField.id().toString()]: { + ...(targetViewMeta[notesField.id().toString()] ?? {}), + hidden: true, + }, + })._unsafeUnwrap(); + + const configuredOtherMeta = ViewColumnMeta.create({ + ...otherViewMeta, + [notesField.id().toString()]: { + ...(otherViewMeta[notesField.id().toString()] ?? {}), + hidden: false, + }, + })._unsafeUnwrap(); + + const configuredTable = TableUpdateViewColumnMetaSpec.create([ + { + viewId: targetView.id(), + fieldId: notesField.id(), + columnMeta: configuredTargetMeta, + }, + { + viewId: otherView.id(), + fieldId: notesField.id(), + columnMeta: configuredOtherMeta, + }, + ]) + .mutate(table) + ._unsafeUnwrap(); + + const newField = SingleLineTextField.create({ + id: newFieldId, + name: FieldName.create('Added')._unsafeUnwrap(), + })._unsafeUnwrap(); + + // Add field with targetViewId but without viewOrder (simulates Add operator) + const updatedTable = configuredTable + .update((mutator) => + mutator.addField(newField, { + targetViewId: targetView.id(), + }) + ) + ._unsafeUnwrap().table; + + const targetEntry = updatedTable.views()[0]?.columnMeta()._unsafeUnwrap().toDto()[ + newField.id().toString() + ]; + const otherEntry = updatedTable.views()[1]?.columnMeta()._unsafeUnwrap().toDto()[ + newField.id().toString() + ]; + + // The new field should NOT be hidden in the target view (current view) + expect(targetEntry?.hidden).toBeUndefined(); + // The new field SHOULD be hidden in the other view that has explicit hidden config + expect(otherEntry?.hidden).toBe(true); + }); + it('rejects adding a field with duplicate dbFieldName', () => { const baseIdResult = createBaseId('d'); const tableNameResult = TableName.create('Duplicate DbFieldName'); diff --git a/packages/v2/core/src/domain/table/TableMutator.ts b/packages/v2/core/src/domain/table/TableMutator.ts index 097b18998d..4cf6606541 100644 --- a/packages/v2/core/src/domain/table/TableMutator.ts +++ b/packages/v2/core/src/domain/table/TableMutator.ts @@ -53,6 +53,7 @@ class TableMutateSpecBuilder extends SpecBuilder; domainContext?: IDomainContext; + targetViewId?: ViewId; viewOrder?: { viewId: ViewId; order: number; @@ -62,7 +63,7 @@ class TableMutateSpecBuilder extends SpecBuilder; + targetViewId?: ViewId; viewOrder?: { viewId: ViewId; order: number;