-
- {selectedRecordLabel}
-
+ const selectedRecordTag = selectedOption ? (
+
+ {selectedOption.label}
) : null;
- const onSelect = (recordId: string) => {
- const idIsAlreadySelected = selectedRecordId === recordId;
+ const onSelect = (record: Record) => {
+ const recordId = record.id;
+ const idIsAlreadySelected = selectedRecord?.id === recordId;
idIsAlreadySelected
? onRemoveRecord() // clear selection
- : onSelectRecord(recordId); // make single selection
+ : onSelectRecord(record); // make single selection
};
return (
@@ -62,9 +57,10 @@ export const PolarisAutoBelongsToInput = autoInput((props: AutoRelationshipInput
id === selectedRecordId}
+ checkSelected={(id) => id === selectedRecord?.id}
onSelect={onSelect}
options={searchFilterOptions}
+ records={relatedModel.records}
/>
>
diff --git a/packages/react/src/auto/polaris/inputs/relationships/PolarisAutoHasManyInput.tsx b/packages/react/src/auto/polaris/inputs/relationships/PolarisAutoHasManyInput.tsx
index cdb6fd4af..bf11fe0c6 100644
--- a/packages/react/src/auto/polaris/inputs/relationships/PolarisAutoHasManyInput.tsx
+++ b/packages/react/src/auto/polaris/inputs/relationships/PolarisAutoHasManyInput.tsx
@@ -1,9 +1,8 @@
-import type { GadgetRecordList } from "@gadgetinc/api-client-core";
-import { Banner, Combobox } from "@shopify/polaris";
-import React from "react";
+import { Combobox } from "@shopify/polaris";
+import React, { useMemo } from "react";
import { autoInput } from "../../../AutoInput.js";
-import { useHasManyInputController } from "../../../hooks/useHasManyInputController.js";
-import { optionRecordsToLoadCount } from "../../../hooks/useRelatedModelOptions.js";
+import { useHasManyInputController } from "../../../hooks/useHasManyController.js";
+import { optionRecordsToLoadCount, useOptionLabelForField } from "../../../hooks/useRelatedModel.js";
import type { AutoRelationshipInputProps } from "../../../interfaces/AutoRelationshipInputProps.js";
import { RelatedModelOptions } from "./RelatedModelOptions.js";
import { getSelectedRelatedRecordTags } from "./SelectedRelatedRecordTags.js";
@@ -13,9 +12,9 @@ export const PolarisAutoHasManyInput = autoInput((props: AutoRelationshipInputPr
const {
fieldMetadata: { path, metadata },
- relatedModelOptions: { options, searchFilterOptions, selected, search, pagination },
+ relatedModelOptions: { options, searchFilterOptions, search, pagination, relatedModel },
- selectedRecordIds,
+ selectedRecords,
errorMessage,
isLoading,
@@ -23,9 +22,15 @@ export const PolarisAutoHasManyInput = autoInput((props: AutoRelationshipInputPr
onRemoveRecord,
} = useHasManyInputController(props);
- if ((selected.records as GadgetRecordList)?.hasNextPage) {
- return {`Too many related records for ${field}. Cannot edit`};
- }
+ const optionLabel = useOptionLabelForField(field, props.optionLabel);
+
+ // if ((selected.records as GadgetRecordList)?.hasNextPage) {
+ // return {`Too many related records for ${field}. Cannot edit`};
+ // }
+
+ const selectedRecordIds = useMemo(() => {
+ return selectedRecords.map((record) => record.id).filter((id) => !!id) as string[];
+ }, [selectedRecords]);
return (
<>
@@ -39,9 +44,9 @@ export const PolarisAutoHasManyInput = autoInput((props: AutoRelationshipInputPr
placeholder="Search"
autoComplete="off"
verticalContent={getSelectedRelatedRecordTags({
- selectedRecordIds,
+ selectedRecords,
onRemoveRecord,
- options,
+ optionLabel,
})}
/>
}
@@ -51,6 +56,7 @@ export const PolarisAutoHasManyInput = autoInput((props: AutoRelationshipInputPr
>
selectedRecordIds.includes(id)}
errorMessage={errorMessage}
diff --git a/packages/react/src/auto/polaris/inputs/relationships/PolarisAutoHasOneInput.tsx b/packages/react/src/auto/polaris/inputs/relationships/PolarisAutoHasOneInput.tsx
index 5bbb99951..e1b9dad63 100644
--- a/packages/react/src/auto/polaris/inputs/relationships/PolarisAutoHasOneInput.tsx
+++ b/packages/react/src/auto/polaris/inputs/relationships/PolarisAutoHasOneInput.tsx
@@ -1,36 +1,32 @@
-import { Banner, Combobox } from "@shopify/polaris";
+import { Combobox, Tag } from "@shopify/polaris";
import React from "react";
import { autoInput } from "../../../AutoInput.js";
import { useHasOneInputController } from "../../../hooks/useHasOneInputController.js";
-import { optionRecordsToLoadCount } from "../../../hooks/useRelatedModelOptions.js";
+import { getRecordAsOption, optionRecordsToLoadCount, useOptionLabelForField } from "../../../hooks/useRelatedModel.js";
import type { AutoRelationshipInputProps } from "../../../interfaces/AutoRelationshipInputProps.js";
import { RelatedModelOptions } from "./RelatedModelOptions.js";
-import { getSelectedRelatedRecordTags } from "./SelectedRelatedRecordTags.js";
-
-/**
- * TODO - Enable when API level 1-1 relationship mappings are maintained by calling updates on other records
- */
-const showErrorBannerWhenTooManyRelatedRecords = false;
export const PolarisAutoHasOneInput = autoInput((props: AutoRelationshipInputProps) => {
const { field } = props;
const {
fieldMetadata: { path, metadata },
- relatedModelOptions: { options, searchFilterOptions, selected, search, pagination },
-
- selectedRecordIds,
+ relatedModelOptions: { options, searchFilterOptions, search, pagination, relatedModel },
+ selectedRecord,
errorMessage,
isLoading,
-
onSelectRecord,
onRemoveRecord,
} = useHasOneInputController(props);
- const hasMultipleRelatedRecords = selected.records && selected.records.length > 1;
+ const optionLabel = useOptionLabelForField(field, props.optionLabel);
+
+ const selectedOption = selectedRecord ? getRecordAsOption(selectedRecord, optionLabel) : null;
- if (showErrorBannerWhenTooManyRelatedRecords && hasMultipleRelatedRecords) {
- return {`Multiple related records for hasOne field "${field}"`};
- }
+ const selectedRecordTag = selectedOption ? (
+ selectedRecord && onRemoveRecord(selectedRecord)} key={`selectedRecordTag_${selectedOption.id}`}>
+ {selectedOption.label}
+
+ ) : null;
return (
<>
@@ -43,11 +39,7 @@ export const PolarisAutoHasOneInput = autoInput((props: AutoRelationshipInputPro
name={path}
placeholder="Search"
autoComplete="off"
- verticalContent={getSelectedRelatedRecordTags({
- selectedRecordIds,
- onRemoveRecord,
- options,
- })}
+ verticalContent={selectedRecordTag}
/>
}
onScrolledToBottom={pagination.loadNextPage}
@@ -56,8 +48,9 @@ export const PolarisAutoHasOneInput = autoInput((props: AutoRelationshipInputPro
selectedRecordIds.includes(id)}
+ checkSelected={(id) => selectedRecord?.id === id}
onSelect={onSelectRecord}
+ records={relatedModel.records}
options={searchFilterOptions}
/>
diff --git a/packages/react/src/auto/polaris/inputs/relationships/RelatedModelOptions.tsx b/packages/react/src/auto/polaris/inputs/relationships/RelatedModelOptions.tsx
index 0f02dae0a..7521e6f05 100644
--- a/packages/react/src/auto/polaris/inputs/relationships/RelatedModelOptions.tsx
+++ b/packages/react/src/auto/polaris/inputs/relationships/RelatedModelOptions.tsx
@@ -4,16 +4,25 @@ import { ListMessage, NoRecordsMessage, SelectableOption, getErrorMessage } from
export const RelatedModelOptions = (props: {
options: { id: string; label: string }[];
+ records?: Record[];
isLoading?: boolean;
errorMessage?: string;
checkSelected?: (id: string) => boolean;
- onSelect: (recordId: string) => void;
+ onSelect: (record: Record) => void;
}) => {
- const { checkSelected, onSelect, isLoading, errorMessage, options } = props;
+ const { checkSelected, onSelect, isLoading, errorMessage, options, records } = props;
return (
-
+ {
+ const record = records?.find((record) => record.id === id) ?? { id };
+
+ const { createdAt: _createdAt, updatedAt: _updatedAt, ...recordWithoutTimestamps } = record;
+
+ onSelect(recordWithoutTimestamps);
+ }}
+ >
{options.length ? (
options.map((option) => )
) : errorMessage ? (
diff --git a/packages/react/src/auto/polaris/inputs/relationships/SelectedRelatedRecordTags.tsx b/packages/react/src/auto/polaris/inputs/relationships/SelectedRelatedRecordTags.tsx
index 7f2bc028b..0f414cb53 100644
--- a/packages/react/src/auto/polaris/inputs/relationships/SelectedRelatedRecordTags.tsx
+++ b/packages/react/src/auto/polaris/inputs/relationships/SelectedRelatedRecordTags.tsx
@@ -1,12 +1,14 @@
import { Tag } from "@shopify/polaris";
import React from "react";
+import { getRecordsAsOptions } from "../../../hooks/useRelatedModel.js";
+import type { OptionLabel } from "../../../interfaces/AutoRelationshipInputProps.js";
export const getSelectedRelatedRecordTags = (props: {
- selectedRecordIds: string[];
- options: { id: string; label: string }[];
- onRemoveRecord: (id: string) => void;
+ selectedRecords: Record[];
+ optionLabel: OptionLabel;
+ onRemoveRecord: (record: Record) => void;
}) => {
- if (!props.selectedRecordIds.length) {
+ if (!props.selectedRecords.length) {
// A separate component getter is used here to return null instead of <>null> which adds extra height to the text field
return null;
}
@@ -14,18 +16,25 @@ export const getSelectedRelatedRecordTags = (props: {
};
export const SelectedRelatedRecordTags = (props: {
- selectedRecordIds: string[];
- options: { id: string; label: string }[];
- onRemoveRecord: (id: string) => void;
+ selectedRecords: Record[];
+ optionLabel: OptionLabel;
+ onRemoveRecord: (record: Record) => void;
}) => {
- const { selectedRecordIds, options, onRemoveRecord } = props;
+ const { selectedRecords, optionLabel, onRemoveRecord } = props;
- return selectedRecordIds.length
- ? selectedRecordIds.map((id) => {
- const option = options.find((option) => option.id === id);
+ const options = getRecordsAsOptions(selectedRecords, optionLabel);
+
+ return options.length
+ ? options.map((option) => {
return (
- onRemoveRecord(id)}>
- {option ? option.label : `id: ${id}`}
+ {
+ const record = selectedRecords.find((record) => record.id === option.id);
+ onRemoveRecord(record ?? { id: option.id });
+ }}
+ >
+ {option.label}
);
})
diff --git a/packages/react/src/metadata.tsx b/packages/react/src/metadata.tsx
index 9d7b628ce..125d57a73 100644
--- a/packages/react/src/metadata.tsx
+++ b/packages/react/src/metadata.tsx
@@ -462,15 +462,15 @@ export const useActionMetadata = (
/**
* @internal
*/
-export const filterAutoFormFieldList = (
+export const buildAutoFormFieldList = (
fields: FieldMetadata[] | undefined,
options?: { include?: string[]; exclude?: string[]; isUpsertAction?: boolean }
-): FieldMetadata[] => {
+): [string, FieldMetadata][] => {
if (!fields) {
return [];
}
- let subset = fields;
+ let subset: [string, FieldMetadata][] = fields.map((field) => [field.apiIdentifier, field]);
if (options?.include && options?.exclude) {
throw new Error("Cannot use both 'include' and 'exclude' options at the same time");
@@ -481,28 +481,57 @@ export const filterAutoFormFieldList = (
subset = [];
const includes = new Set(options.include);
- for (const includedFieldApiId of Array.from(includes)) {
- const metadataField = fields.find((field) => field.apiIdentifier === includedFieldApiId);
+ const includeGroups: Record = {};
+
+ for (const path of Array.from(includes)) {
+ const [rootSegment, ...childSegments] = path.split(".");
+
+ includeGroups[rootSegment] ??= [];
+
+ if (childSegments.length) {
+ includeGroups[rootSegment].push(childSegments.join("."));
+ }
+ }
+
+ for (const [rootApiIdentifier, childIdentifiers] of Object.entries(includeGroups)) {
+ const metadataField = fields.find((field) => field.apiIdentifier === rootApiIdentifier);
if (metadataField) {
- subset.push(metadataField);
+ subset.push([rootApiIdentifier, metadataField]);
+
+ if (
+ childIdentifiers.length > 0 &&
+ "relatedModel" in metadataField.configuration &&
+ metadataField.configuration.relatedModel?.fields
+ ) {
+ const childFields = buildAutoFormFieldList(metadataField.configuration.relatedModel.fields, {
+ include: childIdentifiers,
+ });
+ subset.push(
+ ...childFields.map(
+ ([childApiIdentifier, childField]) => [`${rootApiIdentifier}.${childApiIdentifier}`, childField] as [string, FieldMetadata]
+ )
+ );
+ }
}
}
}
if (options?.exclude) {
const excludes = new Set(options.exclude);
- subset = subset.filter((field) => !excludes.has(field.apiIdentifier));
+ subset = subset.filter(([_, field]) => !excludes.has(field.apiIdentifier));
}
// Remove `hasMany` fields that emerge from `hasManyThrough` fields that are not actually model fields
- subset = subset.filter((field) => !isJoinModelHasManyField(field));
+ subset = subset.filter(([_, field]) => !isJoinModelHasManyField(field));
// Filter out fields that are not supported by the form
- const validFieldTypeSubset = subset.filter(options?.isUpsertAction ? isAcceptedUpsertFieldType : isAcceptedFieldType);
+ const validFieldTypeSubset = subset.filter(([_, field]) =>
+ options?.isUpsertAction ? isAcceptedUpsertFieldType(field) : isAcceptedFieldType(field)
+ );
return options?.include
? validFieldTypeSubset // Everything explicitly included is valid
- : validFieldTypeSubset.filter(isNotRelatedToSpecialModelFilter); // Without explicit includes, filter out relationships to special models
+ : validFieldTypeSubset.filter(([_, field]) => isNotRelatedToSpecialModelFilter(field)); // Without explicit includes, filter out relationships to special models
};
/**
diff --git a/packages/react/src/use-action-form/utils.ts b/packages/react/src/use-action-form/utils.ts
index a04d6a285..3ba424bfe 100644
--- a/packages/react/src/use-action-form/utils.ts
+++ b/packages/react/src/use-action-form/utils.ts
@@ -409,12 +409,11 @@ const getParentRelationshipFieldGraphqlApiInput = (props: { input: any; result:
const { input, result } = props;
const { __typename, ...rest } = result;
- if ("__id" in rest) {
- if ("__unlinkedInverseField" in rest && rest.__unlinkedInverseField) {
- const inverseFieldApiId = rest.__unlinkedInverseField;
- return { update: { id: rest.__id, [inverseFieldApiId]: { _link: null } } };
- }
- return { update: { id: rest.__id } }; // Calling this update action automatically links it to the current parent model's record ID
+ if ("_link" in rest && rest._link) {
+ return { update: { id: rest._link } };
+ } else if ("_unlink" in rest && rest._unlink) {
+ const { id, inverseFieldApiIdentifier } = rest._unlink;
+ return { update: { id, [inverseFieldApiIdentifier]: { _link: null } } };
} else {
const inputHasId = "id" in input;
return inputHasId ? { update: { ...rest } } : { create: { ...rest } };
diff --git a/packages/react/src/use-table/helpers.tsx b/packages/react/src/use-table/helpers.tsx
index 91e9db3bd..987fba49e 100644
--- a/packages/react/src/use-table/helpers.tsx
+++ b/packages/react/src/use-table/helpers.tsx
@@ -297,6 +297,10 @@ const isHasManyOrHasManyThroughField = (field: { fieldType: GadgetFieldType }) =
return field.fieldType === GadgetFieldType.HasMany || field.fieldType === GadgetFieldType.HasManyThrough;
};
+const isRelationshipField = (field: { fieldType: GadgetFieldType }) => {
+ return isHasOneOrBelongsToField(field) || isHasManyOrHasManyThroughField(field);
+};
+
const richTextSelection = {
markdown: true,
truncatedHTML: true,
@@ -400,3 +404,55 @@ const getFieldSelectionErrorMessage = (columnPath: string) => {
RELATED_HAS_ONE_OR_BELONGS_TO_FIELD_NOT_EXIST: `Field '${columnPath}' does not exist in the related model`,
} as const;
};
+
+export const pathListToSelection = (pathList: string[], fieldMetadataArray: FieldMetadata[]) => {
+ const selection: FieldSelection = {
+ id: true,
+ };
+
+ const pathGroups: Record = {};
+
+ for (const path of pathList) {
+ const [rootSegment, ...childSegments] = path.split(".");
+
+ pathGroups[rootSegment] ??= [];
+
+ if (childSegments.length) {
+ pathGroups[rootSegment].push(childSegments.join("."));
+ }
+ }
+
+ for (const [rootPath, childPaths] of Object.entries(pathGroups)) {
+ const field = fieldMetadataArray.find((metadata) => metadata.apiIdentifier == rootPath);
+
+ if (!field) {
+ throw new Error(`No metadata found for ${rootPath}`);
+ }
+
+ if (isRelationshipField(field)) {
+ const configuration = field.configuration;
+ if (configuration && "relatedModel" in configuration && configuration.relatedModel) {
+ const relatedModel = configuration.relatedModel;
+ const relatedSelection = {
+ id: true,
+ [relatedModel?.defaultDisplayField.apiIdentifier]: true,
+ ...(childPaths.length && relatedModel.fields ? pathListToSelection(childPaths, relatedModel.fields) : {}),
+ };
+
+ if (isHasManyOrHasManyThroughField(field)) {
+ selection[field.apiIdentifier] = {
+ edges: {
+ node: relatedSelection,
+ },
+ };
+ } else {
+ selection[field.apiIdentifier] = relatedSelection;
+ }
+ }
+ } else {
+ selection[field.apiIdentifier] = getNonRelationshipSelectionValue(field);
+ }
+ }
+
+ return selection;
+};
diff --git a/packages/react/src/useActionForm.ts b/packages/react/src/useActionForm.ts
index b3910724c..75ff67948 100644
--- a/packages/react/src/useActionForm.ts
+++ b/packages/react/src/useActionForm.ts
@@ -78,6 +78,10 @@ type ActionFormOptions<
* If false, don't throw an error if the the given findBy value is an invalid object
*/
throwOnInvalidFindByObject?: boolean;
+ /**
+ * Whether to pause fetching the record when using findBy.
+ */
+ pause?: boolean;
}
: // eslint-disable-next-line @typescript-eslint/ban-types
{});
@@ -107,6 +111,7 @@ export const useActionForm = <
): UseActionFormResult => {
const findById = options && "findBy" in options ? options.findBy : undefined;
const throwOnInvalidFindByObject = options && "findBy" in options ? options?.throwOnInvalidFindByObject ?? true : true;
+ const pause = options && "pause" in options ? options.pause : undefined;
const api = useApi();
const findExistingRecord = !!findById;
const hasSetInitialValues = useRef(!findExistingRecord);
@@ -116,7 +121,7 @@ export const useActionForm = <
// find the existing record if there is one
const modelManager = isModelAction ? getModelManager(api, action.modelApiIdentifier, action.namespace) : undefined;
const [findResult] = useFindExistingRecord(modelManager, findById || "1", {
- pause: !findExistingRecord,
+ pause: pause || !findExistingRecord,
select: actionSelect,
throwOnInvalidFindByObject,
});