From 482efd9f5a0f752cd63412dc7c5ad77a624a134e Mon Sep 17 00:00:00 2001 From: MillanWangGadget <134947055+MillanWangGadget@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:38:43 -0400 Subject: [PATCH] Revert "Forcefully add special subproperties to the selection of RichText, file, and role fields" --- .../spec/operationBuilders.spec.ts | 94 ------------------- .../api-client-core/src/operationBuilders.ts | 32 +------ .../react/spec/auto/hooks/helpers.spec.ts | 46 --------- packages/react/src/auto/AutoForm.ts | 62 ++++-------- packages/react/src/use-table/helpers.tsx | 6 +- 5 files changed, 25 insertions(+), 215 deletions(-) diff --git a/packages/api-client-core/spec/operationBuilders.spec.ts b/packages/api-client-core/spec/operationBuilders.spec.ts index 523d5f34d..a822cafbd 100644 --- a/packages/api-client-core/spec/operationBuilders.spec.ts +++ b/packages/api-client-core/spec/operationBuilders.spec.ts @@ -1621,98 +1621,4 @@ describe("operation builders", () => { `); }); }); - - describe("selection auto-expansion", () => { - const defaultSelectionWithSpecialFields = { - __typename: true, - id: true, - name: true, - richText: { markdown: true, truncatedHTML: true }, - fileField: { url: true, mimeType: true, fileName: true }, - roleField: { key: true, name: true }, - }; - - describe("findOneOperation", () => { - test("auto-expands richText: true to sub-selection from defaultSelection", () => { - const result = findOneOperation("widget", "123", defaultSelectionWithSpecialFields, "widget", { - select: { id: true, richText: true }, - }); - expect(result.query).toContain("richText"); - expect(result.query).toContain("markdown"); - expect(result.query).toContain("truncatedHTML"); - }); - - test("auto-expands fileField: true to sub-selection from defaultSelection", () => { - const result = findOneOperation("widget", "123", defaultSelectionWithSpecialFields, "widget", { - select: { id: true, fileField: true }, - }); - expect(result.query).toContain("fileField"); - expect(result.query).toContain("url"); - expect(result.query).toContain("mimeType"); - expect(result.query).toContain("fileName"); - }); - - test("auto-expands roleField: true to sub-selection from defaultSelection", () => { - const result = findOneOperation("widget", "123", defaultSelectionWithSpecialFields, "widget", { - select: { id: true, roleField: true }, - }); - expect(result.query).toContain("roleField"); - expect(result.query).toContain("key"); - expect(result.query).toContain("name"); - }); - - test("preserves explicit object selections without overwriting", () => { - const result = findOneOperation("widget", "123", defaultSelectionWithSpecialFields, "widget", { - select: { id: true, richText: { markdown: true } }, - }); - expect(result.query).toContain("markdown"); - expect(result.query).not.toContain("truncatedHTML"); - }); - - test("leaves normal scalar fields untouched", () => { - const result = findOneOperation("widget", "123", defaultSelectionWithSpecialFields, "widget", { select: { id: true, name: true } }); - expect(result.query).toContain("id"); - expect(result.query).toContain("name"); - expect(result.query).not.toContain("richText"); - }); - }); - - describe("findManyOperation", () => { - test("auto-expands richText: true in findMany", () => { - const result = findManyOperation("widgets", defaultSelectionWithSpecialFields, "widget", { - select: { id: true, richText: true }, - }); - expect(result.query).toContain("richText"); - expect(result.query).toContain("markdown"); - expect(result.query).toContain("truncatedHTML"); - }); - - test("auto-expands fileField: true in findMany", () => { - const result = findManyOperation("widgets", defaultSelectionWithSpecialFields, "widget", { - select: { id: true, fileField: true }, - }); - expect(result.query).toContain("fileField"); - expect(result.query).toContain("url"); - expect(result.query).toContain("mimeType"); - }); - }); - - describe("actionOperation", () => { - test("auto-expands richText: true in action", () => { - const result = actionOperation( - "createWidget", - defaultSelectionWithSpecialFields, - "widget", - "widget", - { - widget: { type: "CreateWidgetInput", value: { name: "test" } }, - }, - { select: { id: true, richText: true } } - ); - expect(result.query).toContain("richText"); - expect(result.query).toContain("markdown"); - expect(result.query).toContain("truncatedHTML"); - }); - }); - }); }); diff --git a/packages/api-client-core/src/operationBuilders.ts b/packages/api-client-core/src/operationBuilders.ts index 3dbbb4681..f193615da 100644 --- a/packages/api-client-core/src/operationBuilders.ts +++ b/packages/api-client-core/src/operationBuilders.ts @@ -24,30 +24,6 @@ const fieldSelectionToQueryCompilerFields = (selection: FieldSelection, includeT export type FindFirstPaginationOptions = Omit; -/** - * When a user passes `{ field: true }` for a field that requires sub-selections - * (like richText, file, or role fields), auto-expand it using the defaultSelection. - */ -const normalizeSelection = (selection: FieldSelection, defaultSelection: FieldSelection): FieldSelection => { - const result: FieldSelection = {}; - for (const [key, value] of Object.entries(selection)) { - const defaultValue = defaultSelection[key]; - if (value === true && defaultValue && typeof defaultValue === "object") { - result[key] = defaultValue; - } else if (value && typeof value === "object" && defaultValue && typeof defaultValue === "object") { - result[key] = normalizeSelection(value as FieldSelection, defaultValue as FieldSelection); - } else { - result[key] = value; - } - } - return result; -}; - -const resolveSelection = (defaultSelection: FieldSelection, userSelection?: FieldSelection | null): FieldSelection => { - if (!userSelection) return defaultSelection; - return normalizeSelection(userSelection, defaultSelection); -}; - const directivesForOptions = (options?: BaseFindOptions | null) => { if (options?.live) return ["@live"]; return undefined; @@ -65,7 +41,7 @@ export const findOneOperation = ( if (typeof id !== "undefined") variables.id = Var({ type: "GadgetID!", value: id }); let fields = { - [operation]: Call(variables, fieldSelectionToQueryCompilerFields(resolveSelection(defaultSelection, options?.select), true)), + [operation]: Call(variables, fieldSelectionToQueryCompilerFields(options?.select || defaultSelection, true)), }; fields = namespacify(namespace, fields); @@ -129,7 +105,7 @@ export const findManyOperation = ( pageInfo: { hasNextPage: true, hasPreviousPage: true, startCursor: true, endCursor: true }, edges: { cursor: true, - node: fieldSelectionToQueryCompilerFields(resolveSelection(defaultSelection, options?.select), true), + node: fieldSelectionToQueryCompilerFields(options?.select || defaultSelection, true), }, } ), @@ -200,7 +176,7 @@ export const actionOperation = ( isBulkAction?: boolean | null, hasReturnType?: HasReturnType | null ) => { - const selection = resolveSelection(defaultSelection!, options?.select); + const selection = options?.select || defaultSelection; let fields: BuilderFieldSelection = { [operation]: Call( @@ -247,7 +223,7 @@ export const backgroundActionResultOperation = { }); }); }); - - describe("exported selection constants", () => { - it("richTextSelection should contain markdown and truncatedHTML", () => { - expect(richTextSelection).toEqual({ - markdown: true, - truncatedHTML: true, - }); - }); - - it("fileSelection should contain url, mimeType, and fileName", () => { - expect(fileSelection).toEqual({ - url: true, - mimeType: true, - fileName: true, - }); - }); - - it("roleAssignmentsSelection should contain key and name", () => { - expect(roleAssignmentsSelection).toEqual({ - key: true, - name: true, - }); - }); - - it("getTableSelectionMap uses the exported selection constants for special field types", () => { - const result = getTableSelectionMap({ - targetColumns: ["description", "image", "roles"], - fieldMetadataTree: fieldMetadataArrayToFieldMetadataTree([ - getSimpleFieldMetadata("Description", "description", GadgetFieldType.RichText), - getSimpleFieldMetadata("Image", "image", GadgetFieldType.File), - getSimpleFieldMetadata("Roles", "roles", GadgetFieldType.RoleAssignments), - ]), - defaultSelection: {}, - }); - - expect(result).toEqual({ - id: true, - description: richTextSelection, - image: fileSelection, - roles: roleAssignmentsSelection, - }); - }); - }); }); const gadgetGenericFieldConfig = { diff --git a/packages/react/src/auto/AutoForm.ts b/packages/react/src/auto/AutoForm.ts index 1c3b332cb..24aae26f1 100644 --- a/packages/react/src/auto/AutoForm.ts +++ b/packages/react/src/auto/AutoForm.ts @@ -22,15 +22,7 @@ import type { UseActionFormSubmit, } from "../use-action-form/types.js"; import { isPlainObject, processDefaultValues, toDefaultValues } from "../use-action-form/utils.js"; -import { - fileSelection, - getRelatedModelFields, - isHasManyOrHasManyThroughField, - isRelationshipField, - pathListToSelection, - richTextSelection, - roleAssignmentsSelection, -} from "../use-table/helpers.js"; +import { getRelatedModelFields, isHasManyOrHasManyThroughField, isRelationshipField, pathListToSelection } from "../use-table/helpers.js"; import type { FieldErrors, FieldValues, UseFormReturn } from "../useActionForm.js"; import { useActionForm } from "../useActionForm.js"; import { get, getFlattenedObjectKeys, set } from "../utils.js"; @@ -229,7 +221,7 @@ const useFormSelection = (props: { if (!select || !modelApiIdentifier) { return; } - return forceRequiredFieldsIntoSelect({ select, rootFieldsMetadata }); + return forceIdsIntoSelect({ select, rootFieldsMetadata }); }, [select, modelApiIdentifier, rootFieldsMetadata]); if (!modelApiIdentifier || !fields.length) { @@ -246,56 +238,38 @@ const useFormSelection = (props: { return pathListToSelection(modelApiIdentifier, paths, fieldMetaData); }; -const forceRequiredFieldsIntoSelect = (props: { select: FieldSelection; rootFieldsMetadata: FieldMetadata[] }) => { +const forceIdsIntoSelect = (props: { select: FieldSelection; rootFieldsMetadata: FieldMetadata[] }) => { const { select: originalSelect, rootFieldsMetadata } = props; const select = structuredClone(originalSelect); select.id = true; // Always select the ID for the root model - const addRequiredFieldsToSelection = (selectPath: string, fieldMetadata: FieldMetadata) => { - const isRichTextField = fieldMetadata.fieldType === FieldType.RichText; - const isFileField = fieldMetadata.fieldType === FieldType.File; - const isRolesField = fieldMetadata.fieldType === FieldType.RoleAssignments; - const isRelationship = isRelationshipField(fieldMetadata); - - const existingSelection = get(select, selectPath); - if (!existingSelection) { - return; // Do not select at all - } - - if (isRichTextField) { - return set(select, selectPath, richTextSelection); // Assume that the whole rich text is expected to be selected + const addIdToSelection = (selectPath: string, fieldMetadata: FieldMetadata) => { + if (!isRelationshipField(fieldMetadata)) { + return; // Non relationships do not need additional selection } - if (isFileField) { - return set(select, selectPath, fileSelection); // Assume whole file is expected to be selected - } - if (isRolesField) { - return set(select, selectPath, roleAssignmentsSelection); // Assume whole role assignments are expected to be selected + const existingSelection = get(select, selectPath); + if (!existingSelection || typeof existingSelection !== "object") { + // Do not go deeper than what is defined in the select object + return; } - if (isRelationship) { - if (typeof existingSelection !== "object") { - // Do not go deeper than what is defined in the select object - return; - } + const isManyRelation = isHasManyOrHasManyThroughField(fieldMetadata); + const currentFieldSelectPathPrefix = isManyRelation ? `${selectPath}.edges.node` : `${selectPath}`; + const idPath = `${currentFieldSelectPathPrefix}.id`; - const isManyRelation = isHasManyOrHasManyThroughField(fieldMetadata); - const currentFieldSelectPathPrefix = isManyRelation ? `${selectPath}.edges.node` : `${selectPath}`; - const idPath = `${currentFieldSelectPathPrefix}.id`; + set(select, idPath, true); - set(select, idPath, true); + const relatedModelFields = getRelatedModelFields(fieldMetadata); - const relatedModelFields = getRelatedModelFields(fieldMetadata); - - for (const relatedModelField of relatedModelFields) { - addRequiredFieldsToSelection(`${currentFieldSelectPathPrefix}.${relatedModelField.apiIdentifier}`, relatedModelField); - } + for (const relatedModelField of relatedModelFields) { + addIdToSelection(`${currentFieldSelectPathPrefix}.${relatedModelField.apiIdentifier}`, relatedModelField); } }; for (const field of rootFieldsMetadata) { - addRequiredFieldsToSelection(field.apiIdentifier, field); + addIdToSelection(field.apiIdentifier, field); } return select; diff --git a/packages/react/src/use-table/helpers.tsx b/packages/react/src/use-table/helpers.tsx index a78aaedca..b23ed5802 100644 --- a/packages/react/src/use-table/helpers.tsx +++ b/packages/react/src/use-table/helpers.tsx @@ -379,18 +379,18 @@ export const isRelationshipField = (field: { fieldType: GadgetFieldType }) => { return isHasOneOrBelongsToField(field) || isHasManyOrHasManyThroughField(field); }; -export const richTextSelection = { +const richTextSelection = { markdown: true, truncatedHTML: true, }; -export const fileSelection = { +const fileSelection = { url: true, mimeType: true, fileName: true, }; -export const roleAssignmentsSelection = { +const roleAssignmentsSelection = { key: true, name: true, };