diff --git a/plugins/hubspot/src/App.tsx b/plugins/hubspot/src/App.tsx index 402f7272..a3e44d99 100644 --- a/plugins/hubspot/src/App.tsx +++ b/plugins/hubspot/src/App.tsx @@ -148,7 +148,7 @@ export function App() { return syncBlogs({ includedFieldIds: blogContext.includedFieldIds, fields: blogContext.collectionFields, - }).then(() => framer.closePlugin("Synchronization successful")) + }).then(() => framer.closePlugin()) } if (shouldSyncHubDB) { @@ -157,7 +157,7 @@ export function App() { fields: hubContext.collectionFields, slugFieldId: hubContext.slugFieldId, includedFieldIds: hubContext.includedFieldIds, - }).then(() => framer.closePlugin("Synchronization successful")) + }).then(() => framer.closePlugin()) } if (blogContext.type === "update") { diff --git a/plugins/hubspot/src/api.ts b/plugins/hubspot/src/api.ts index f4f47139..03113b23 100644 --- a/plugins/hubspot/src/api.ts +++ b/plugins/hubspot/src/api.ts @@ -51,7 +51,21 @@ export interface HubDBFile { type: "file" } -export type HubDBCellValue = string | number | boolean | Date | HubDBImage | HubDBValueOption | HubDBFile +export type HubDBForeignValues = Array<{ + id: string + name: string + type: "foreignid" +}> + +export type HubDBCellValue = + | string + | number + | boolean + | Date + | HubDBImage + | HubDBValueOption + | HubDBFile + | HubDBForeignValues export interface HSAccount { portalId: number diff --git a/plugins/hubspot/src/components/FieldMapper.tsx b/plugins/hubspot/src/components/FieldMapper.tsx index e24e9372..feade3d0 100644 --- a/plugins/hubspot/src/components/FieldMapper.tsx +++ b/plugins/hubspot/src/components/FieldMapper.tsx @@ -22,6 +22,7 @@ interface FieldMapperProps { className?: string height?: number } + const getInitialSortedFields = ( fields: ManagedCollectionFieldConfig[], isFieldSelected: (fieldId: string) => boolean @@ -34,9 +35,17 @@ const getInitialSortedFields = ( if (aIsSelected && !bIsSelected) return -1 if (!aIsSelected && bIsSelected) return 1 - // Then sort by whether they are supported fields - if (a.field && !b.field) return -1 - if (!a.field && b.field) return 1 + // Sort by whether they are supported fields + if (a.field !== null && a.field !== undefined && (b.field === null || b.field === undefined)) return -1 + if ((a.field === null || a.field === undefined) && b.field !== null && b.field !== undefined) return 1 + + // Sort by whether they are null (missing reference) + if (a.field === null && b.field !== null) return -1 + if (a.field !== null && b.field === null) return 1 + + // Sort by whether they are undefined (unsupported fields) + if (a.field === undefined && b.field !== undefined) return 1 + if (a.field !== undefined && b.field === undefined) return -1 return 0 }) @@ -93,9 +102,11 @@ export const FieldMapper = ({ disabled={!fieldConfig.field || !isSelected} placeholder={fieldConfig.originalFieldName} value={ - !fieldConfig.field + fieldConfig.field === undefined ? "Unsupported Field" - : (fieldNameOverrides[fieldConfig.field.id] ?? "") + : fieldConfig.field === null + ? "Missing Reference" + : (fieldNameOverrides[fieldConfig.field.id] ?? "") } onChange={e => { assert(fieldConfig.field) diff --git a/plugins/hubspot/src/hubdb.ts b/plugins/hubspot/src/hubdb.ts index 02aff758..6a533fb2 100644 --- a/plugins/hubspot/src/hubdb.ts +++ b/plugins/hubspot/src/hubdb.ts @@ -10,6 +10,7 @@ import { HubDBValueOption, HubDBImage, HubDBFile, + HubDBForeignValues, } from "./api" import { slugify, @@ -30,6 +31,8 @@ const PLUGIN_SLUG_FIELD_ID_KEY = "hubdbSlugFieldId" // Public HubSpot apps have a max of 100 requests / 10s const CONCURRENCY_LIMIT = 5 +export type TableIdMap = Map + export interface SyncMutationOptions { fields: ManagedCollectionField[] tableId: string @@ -50,6 +53,7 @@ export interface ProcessRowParams { export interface HubDBPluginContextNew { type: "new" collection: ManagedCollection + tableIdMap: TableIdMap } export interface HubDBPluginContextUpdate { @@ -61,10 +65,24 @@ export interface HubDBPluginContextUpdate { slugFieldId: string columns: Column[] collectionFields: ManagedCollectionField[] + tableIdMap: TableIdMap } export type HubDBPluginContext = HubDBPluginContextNew | HubDBPluginContextUpdate +export async function getTableIdMap(): Promise { + const tableIdMap: TableIdMap = new Map() + + for (const collection of await framer.getCollections()) { + const collectionTableId = await collection.getPluginData(PLUGIN_TABLE_ID_KEY) + if (!collectionTableId) continue + + tableIdMap.set(collectionTableId, collection.id) + } + + return tableIdMap +} + /** * Get the value of a HubDB cell in a format compatible with a collection field. */ @@ -97,6 +115,9 @@ async function getFieldValue(column: Column, cellValue: HubDBCellValue): Promise case "CURRENCY": return typeof cellValue === "number" ? cellValue : undefined + case "FOREIGN_ID": + return (cellValue as HubDBForeignValues).map(({ id }) => id) + default: return undefined } @@ -104,8 +125,13 @@ async function getFieldValue(column: Column, cellValue: HubDBCellValue): Promise /** * Get the collection field schema for a HubDB column. + * Returns `null` for fields that are supported but can't be synced yet + * Returns `undefined` for fields that are not supported outright */ -export function getCollectionFieldForHubDBColumn(column: Column): ManagedCollectionField | null { +export function getCollectionFieldForHubDBColumn( + column: Column, + tableIdMap: TableIdMap +): ManagedCollectionField | null { assert(column.id) const fieldMetadata = { @@ -155,10 +181,28 @@ export function getCollectionFieldForHubDBColumn(column: Column): ManagedCollect } } - // TODO: Implement collection references - case "FOREIGN_ID": + case "FOREIGN_ID": { + const foreignTableId = column.foreignTableId + assert(foreignTableId) + + const collectionId = tableIdMap.get(String(foreignTableId)) + if (!collectionId) { + // Table includes a relation to a table that hasn't been synced to Framer. + // TODO: It would be better to surface this error to the user in + // the UI instead of just skipping the field. + // - same as Notion + return null + } + + return { + ...fieldMetadata, + type: "multiCollectionReference", + collectionId, + } + } + default: - return null + return undefined } } @@ -196,7 +240,8 @@ export function shouldSyncHubDBImmediately( function hasFieldConfigurationChanged( currentManagedCollectionFields: ManagedCollectionField[], columns: Column[], - includedFieldIds: string[] + includedFieldIds: string[], + tableIdMap: TableIdMap ): boolean { const currentFieldsById = new Map(currentManagedCollectionFields.map(field => [field.id, field])) @@ -210,7 +255,7 @@ function hasFieldConfigurationChanged( for (const column of includedColumns) { assert(column.id) const collectionField = currentFieldsById.get(column.id) - const expectedField = getCollectionFieldForHubDBColumn(column) + const expectedField = getCollectionFieldForHubDBColumn(column, tableIdMap) if (!collectionField) { return true @@ -352,8 +397,10 @@ export async function getHubDBPluginContext(): Promise { collection.getPluginData(PLUGIN_SLUG_FIELD_ID_KEY), ]) + const tableIdMap = await getTableIdMap() + if (!tableId || !slugFieldId || !rawIncludedFieldHash) { - return { type: "new", collection } + return { type: "new", collection, tableIdMap } } const { columns } = await fetchPublishedTable(tableId) @@ -371,7 +418,7 @@ export async function getHubDBPluginContext(): Promise { hasChangedFields = true } else { // Do full check - hasChangedFields = hasFieldConfigurationChanged(collectionFields, columns, includedFieldIds) + hasChangedFields = hasFieldConfigurationChanged(collectionFields, columns, includedFieldIds, tableIdMap) } return { @@ -383,6 +430,7 @@ export async function getHubDBPluginContext(): Promise { slugFieldId, collectionFields, collection, + tableIdMap, } } diff --git a/plugins/hubspot/src/pages/cms/hubdb/MapFields.tsx b/plugins/hubspot/src/pages/cms/hubdb/MapFields.tsx index 1c2f51ca..bc5db398 100644 --- a/plugins/hubspot/src/pages/cms/hubdb/MapFields.tsx +++ b/plugins/hubspot/src/pages/cms/hubdb/MapFields.tsx @@ -7,6 +7,7 @@ import { getCollectionFieldForHubDBColumn, getPossibleSlugFields, HubDBPluginContext, + TableIdMap, useSyncHubDBTableMutation, } from "@/hubdb" import { PageProps } from "@/router" @@ -22,9 +23,9 @@ const getInitialSlugFieldId = (context: HubDBPluginContext, columns: Column[]): return textColumns[0].id ?? null } -const createFieldConfig = (columns: Column[]): ManagedCollectionFieldConfig[] => { +const createFieldConfig = (columns: Column[], tableIdMap: TableIdMap): ManagedCollectionFieldConfig[] => { return columns.map(col => ({ - field: getCollectionFieldForHubDBColumn(col), + field: getCollectionFieldForHubDBColumn(col, tableIdMap), originalFieldName: col.label, })) } @@ -65,7 +66,7 @@ export default function MapHubDBFieldsPage({ hubDbPluginContext }: PageProps) { ) setSlugFieldId(getInitialSlugFieldId(hubDbPluginContext, columns)) - setCollectionFieldConfig(createFieldConfig(columns)) + setCollectionFieldConfig(createFieldConfig(columns, hubDbPluginContext.tableIdMap)) setIncludedFieldIds(newIncludedFieldIds) setFieldNameOverrides(getFieldNameOverrides(hubDbPluginContext)) }, [hubDbPluginContext, table])