From 35a6c2ac978d3074725078dca77956793a5666a4 Mon Sep 17 00:00:00 2001 From: djohalo2 Date: Fri, 17 Jul 2026 09:45:24 +0200 Subject: [PATCH 1/7] feat: add google sheets image alt text support --- plugins/google-sheets/src/App.tsx | 16 +- .../google-sheets/src/components/Icons.tsx | 13 ++ .../src/pages/MapSheetFields.tsx | 147 ++++++++++++++---- plugins/google-sheets/src/sheets.ts | 99 ++++++++++-- 4 files changed, 223 insertions(+), 52 deletions(-) diff --git a/plugins/google-sheets/src/App.tsx b/plugins/google-sheets/src/App.tsx index 10cdc0239..7a842113a 100644 --- a/plugins/google-sheets/src/App.tsx +++ b/plugins/google-sheets/src/App.tsx @@ -15,7 +15,7 @@ import { useSyncSheetMutation, } from "./sheets" import { showFieldMappingUI, showLoginUI } from "./ui" -import { assert, syncMethods } from "./utils" +import { assert, generateUniqueNames, syncMethods } from "./utils" interface AppProps { pluginContext: PluginContext @@ -209,8 +209,10 @@ export function App({ pluginContext }: AppProps) { slugColumn, lastSyncedTime, sheet, + altTextAssignments, } = context const [headerRow] = sheet.values + const uniqueHeaderRowNames = generateUniqueNames(headerRow) const task = async () => { try { @@ -224,10 +226,14 @@ export function App({ pluginContext }: AppProps) { spreadsheetId, sheetTitle, fields, - // Determine if the field type is already configured, otherwise default to "string" - colFieldTypes: headerRow.map(colName => { - const field = fields.find(field => field.name === colName) - return field?.type ?? "string" + // Determine if the field type is already configured, otherwise default to "string". + // Alt text columns never become real CMS fields, so `fields` won't have them. + columnConfigs: uniqueHeaderRowNames.map(columnId => { + if (columnId in altTextAssignments) { + return { type: "altText" as const, imageFieldId: altTextAssignments[columnId] } + } + const field = fields.find(field => field.id === columnId) + return { type: field?.type ?? "string" } }), configureFields: false, }) diff --git a/plugins/google-sheets/src/components/Icons.tsx b/plugins/google-sheets/src/components/Icons.tsx index c358ccf3a..87a93103a 100644 --- a/plugins/google-sheets/src/components/Icons.tsx +++ b/plugins/google-sheets/src/components/Icons.tsx @@ -10,3 +10,16 @@ export const IconChevron = () => ( > ) + +export const IconChevronDown = () => ( + +) diff --git a/plugins/google-sheets/src/pages/MapSheetFields.tsx b/plugins/google-sheets/src/pages/MapSheetFields.tsx index e9cc36bf5..0aaa43376 100644 --- a/plugins/google-sheets/src/pages/MapSheetFields.tsx +++ b/plugins/google-sheets/src/pages/MapSheetFields.tsx @@ -1,14 +1,15 @@ import cx from "classnames" -import { framer, useIsAllowedTo } from "framer-plugin" +import { framer, type MenuItem, useIsAllowedTo } from "framer-plugin" import { Fragment, useMemo, useState } from "react" import { CheckboxTextfield } from "../components/CheckboxTextField" -import { IconChevron } from "../components/Icons" +import { IconChevron, IconChevronDown } from "../components/Icons" import type { CellValue, HeaderRow, PluginContext, Row, SheetCollectionFieldInput, + SheetColumnConfig, SyncMutationOptions, VirtualFieldType, } from "../sheets" @@ -103,15 +104,19 @@ const inferFieldType = (cellValue: CellValue): VirtualFieldType => { return "string" } -const getFieldType = (context: PluginContext, columnId: string, cellValue?: CellValue): VirtualFieldType => { +const getColumnConfig = (context: PluginContext, columnId: string, cellValue?: CellValue): SheetColumnConfig => { + if (context.type === "update" && columnId in context.altTextAssignments) { + return { type: "altText", imageFieldId: context.altTextAssignments[columnId] } + } + // Determine if the field type is already configured if ("collectionFields" in context) { const field = context.collectionFields.find(field => field.id === columnId) - return field?.type ?? "string" + return { type: field?.type ?? "string" } } // Otherwise, infer the field type from the cell value - return cellValue ? inferFieldType(cellValue) : "string" + return { type: cellValue ? inferFieldType(cellValue) : "string" } } const createFieldConfig = ( @@ -125,10 +130,13 @@ const createFieldConfig = ( const sanitizedName = uniqueColumnNames[columnIndex] if (!sanitizedName) return null + const { type, imageFieldId } = getColumnConfig(context, sanitizedName, row?.[columnIndex]) + return { id: sanitizedName, name: sanitizedName, - type: getFieldType(context, sanitizedName, row?.[columnIndex]), + type, + imageFieldId, } as SheetCollectionFieldInput }) .filter(isDefined) @@ -149,6 +157,19 @@ const getPossibleSlugFields = (fieldConfig: SheetCollectionFieldInput[]): SheetC return fieldConfig.filter(field => field.type === "string") } +const getFieldTypeLabel = (field: SheetCollectionFieldInput, allFields: SheetCollectionFieldInput[]): string => { + if (field.type === "altText") { + const imageField = allFields.find(f => f.id === field.imageFieldId) + return imageField ? `Alt Text → ${imageField.name}` : "Alt Text" + } + + return fieldTypeOptions.find(option => option.type === field.type)?.label ?? field.type +} + +const getAltTextImageField = (field: SheetCollectionFieldInput, allFields: SheetCollectionFieldInput[]) => { + return allFields.find(candidate => candidate.id === field.imageFieldId) +} + interface Props { spreadsheetId: string sheetTitle: string @@ -200,13 +221,14 @@ export function MapSheetFieldsPage({ })) } - const handleFieldTypeChange = (id: string, type: VirtualFieldType) => { + const handleFieldTypeChange = (id: string, type: VirtualFieldType, imageFieldId?: string) => { setFieldConfig(current => current.map(field => { if (field.id === id) { return { ...field, type, + imageFieldId: type === "altText" ? imageFieldId : undefined, } as SheetCollectionFieldInput } return field @@ -214,13 +236,52 @@ export function MapSheetFieldsPage({ ) } + const handleFieldTypeMenuOpen = (field: SheetCollectionFieldInput, e: React.MouseEvent) => { + const imageFields = fieldConfig.filter( + f => f.type === "image" && f.id !== field.id && !disabledColumns.has(f.id) + ) + const { left, bottom, width } = e.currentTarget.getBoundingClientRect() + + const items: MenuItem[] = [ + ...fieldTypeOptions.map(({ type, label }) => ({ + label, + checked: field.type === type, + onAction: () => { + handleFieldTypeChange(field.id, type) + }, + })), + { + label: "Alt Text", + checked: field.type === "altText", + enabled: imageFields.length > 0, + submenu: imageFields.map(imgField => ({ + label: imgField.name, + checked: field.type === "altText" && field.imageFieldId === imgField.id, + onAction: () => { + handleFieldTypeChange(field.id, "altText", imgField.id) + }, + })), + }, + ] + + const locationX = left + width - 4; + const locationY = bottom + 4; + + void framer.showContextMenu(items, { + location: { x: locationX, y: locationY }, + placement: "bottom-left", + width, + }) + } + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (isPending) return const allFields = fieldConfig - .filter(field => !disabledColumns.has(field.id)) + // Alt text columns are virtual: they never become their own CMS field. + .filter(field => !disabledColumns.has(field.id) && field.type !== "altText") .map(field => { const maybeOverride = fieldNameOverrides[field.id] if (maybeOverride) { @@ -236,7 +297,7 @@ export function MapSheetFieldsPage({ fields: allFields, spreadsheetId, sheetTitle, - colFieldTypes: fieldConfig.map(field => field.type), + columnConfigs: fieldConfig.map(field => ({ type: field.type, imageFieldId: field.imageFieldId })), ignoredColumns: Array.from(disabledColumns), slugColumn, lastSyncedTime: getLastSyncedTime(pluginContext, slugColumn), @@ -290,31 +351,55 @@ export function MapSheetFieldsPage({
- - { - handleFieldNameChange(field.id, e.target.value) - }} - /> + {field.type === "altText" ? ( + + + Alt Text + + + + {getAltTextImageField(field, fieldConfig)?.name ?? "Image"} + + + ) : ( + + {getFieldTypeLabel(field, fieldConfig)} + + )} + + + + + {field.type !== "altText" ? ( + { + handleFieldNameChange(field.id, e.target.value) + }} + /> + ) : ( +
+ )} ) })} diff --git a/plugins/google-sheets/src/sheets.ts b/plugins/google-sheets/src/sheets.ts index 106d4ae52..70ccee268 100644 --- a/plugins/google-sheets/src/sheets.ts +++ b/plugins/google-sheets/src/sheets.ts @@ -31,6 +31,7 @@ const PLUGIN_LAST_SYNCED_KEY = "sheetsPluginLastSynced" const PLUGIN_IGNORED_COLUMNS_KEY = "sheetsPluginIgnoredColumns" const PLUGIN_SHEET_HEADER_ROW_HASH_KEY = "sheetsPluginSheetHeaderRowHash" const PLUGIN_SLUG_COLUMN_KEY = "sheetsPluginSlugColumn" +const PLUGIN_ALT_TEXT_ASSIGNMENTS_KEY = "sheetsPluginAltTextAssignments" const CELL_BOOLEAN_VALUES = ["Y", "yes", "true", "TRUE", "Yes", 1, true] const HEADER_ROW_DELIMITER = "OIhpKTpp" @@ -235,9 +236,15 @@ function fetchSheetWithClient(spreadsheetId: string, sheetTitle: string, range?: } export type CollectionFieldType = ManagedCollectionFieldInput["type"] -export type VirtualFieldType = CollectionFieldType | "dateTime" +export type VirtualFieldType = CollectionFieldType | "dateTime" | "altText" export type SheetCollectionFieldInput = Omit & { type: VirtualFieldType + imageFieldId?: string +} + +export interface SheetColumnConfig { + type: VirtualFieldType + imageFieldId?: string } export interface PluginContextNew { @@ -259,6 +266,7 @@ export interface PluginContextUpdate { sheet: Sheet lastSyncedTime: string sheetHeaderRow: string[] + altTextAssignments: Record } export interface PluginContextNoSheetAccess { @@ -296,13 +304,14 @@ export interface SyncResult extends SyncStatus { } interface ProcessSheetRowParams { - fieldTypes: VirtualFieldType[] + columnConfigs: SheetColumnConfig[] row: Row rowIndex: number columnCount: number uniqueHeaderRowNames: string[] slugFieldColumnIndex: number ignoredFieldColumnIndexes: number[] + imageAltColumnIndexes: Map status: SyncStatus } @@ -313,7 +322,7 @@ export interface SyncMutationOptions { fields: SheetCollectionFieldInput[] slugColumn: string | null ignoredColumns: string[] - colFieldTypes: VirtualFieldType[] + columnConfigs: SheetColumnConfig[] lastSyncedTime: string | null /** * When false (sync-only mode), schema is only applied when enum fields need refreshed cases. @@ -410,7 +419,23 @@ function enrichFieldsWithEnumCases( }) } -function getFieldDataEntryInput(type: VirtualFieldType, cellValue: CellValue): FieldDataEntryInput | null { +function getAltTextAssignments(columnConfigs: SheetColumnConfig[], uniqueHeaderRowNames: string[]) { + return columnConfigs.reduce>((assignments, column, i) => { + if (column.type !== "altText") return assignments + + const imageFieldId = column.imageFieldId + const altColumnId = uniqueHeaderRowNames[i] + if (!imageFieldId || !altColumnId) return assignments + + return { ...assignments, [altColumnId]: imageFieldId } + }, {}) +} + +function getFieldDataEntryInput( + type: VirtualFieldType, + cellValue: CellValue, + altCellValue?: CellValue +): FieldDataEntryInput | null { switch (type) { case "number": { const num = Number(cellValue) @@ -442,7 +467,14 @@ function getFieldDataEntryInput(type: VirtualFieldType, cellValue: CellValue): F return null } - case "image": + case "image": { + if (!isDefined(cellValue)) return null + return { + type: "image", + value: String(cellValue), + alt: isDefined(altCellValue) ? String(altCellValue) : undefined, + } + } case "link": case "file": case "formattedText": @@ -467,8 +499,9 @@ function processSheetRow({ uniqueHeaderRowNames, ignoredFieldColumnIndexes, slugFieldColumnIndex, + imageAltColumnIndexes, status, - fieldTypes, + columnConfigs, }: ProcessSheetRowParams) { const fieldData: FieldDataInput = {} let slugValue: string | null = null @@ -477,16 +510,22 @@ function processSheetRow({ for (let i = 0; i < columnCount; i++) { const cell = row[i] ?? null - const fieldType = fieldTypes[i] + const fieldType = columnConfigs[i]?.type if (!fieldType) continue + // Alt text columns are virtual and never become their own field + if (fieldType === "altText") continue + // Skip processing ignored columns unless they are the slug field const isIgnored = ignoredFieldColumnIndexes.includes(i) const isSlugField = i === slugFieldColumnIndex if (isIgnored && !isSlugField) continue - let fieldDataEntryInput = getFieldDataEntryInput(fieldType, cell) + const altColumnIndex = imageAltColumnIndexes.get(i) + const altCell = isDefined(altColumnIndex) ? (row[altColumnIndex] ?? null) : undefined + + let fieldDataEntryInput = getFieldDataEntryInput(fieldType, cell, altCell) // Set to default value for type if no value is provided if (!fieldDataEntryInput) { @@ -636,7 +675,7 @@ export async function syncSheet({ fields, ignoredColumns, slugColumn, - colFieldTypes, + columnConfigs, configureFields, }: SyncMutationOptions) { if (fields.length === 0) { @@ -649,6 +688,9 @@ export async function syncSheet({ const [headerRow, ...rows] = sheet.values const uniqueHeaderRowNames = generateUniqueNames(headerRow) + + const altTextAssignments = getAltTextAssignments(columnConfigs, uniqueHeaderRowNames) + const enrichedFields = enrichFieldsWithEnumCases(fields, uniqueHeaderRowNames, rows) const needsFieldSchemaUpdate = configureFields || enrichedFields.some(field => field.type === "enum") @@ -680,11 +722,26 @@ export async function syncSheet({ ) } + const imageAltColumnIndexes = new Map() + for (let i = 0; i < columnConfigs.length; i++) { + if (columnConfigs[i]?.type !== "altText") continue + if (ignoredColumns.includes(uniqueHeaderRowNames[i] ?? "")) continue + + const imageFieldId = columnConfigs[i]?.imageFieldId + if (!imageFieldId || ignoredColumns.includes(imageFieldId)) continue + + const imageColIndex = uniqueHeaderRowNames.indexOf(imageFieldId) + if (imageColIndex === -1 || columnConfigs[imageColIndex]?.type !== "image") continue + + imageAltColumnIndexes.set(imageColIndex, i) + } + const { collectionItems, status } = processSheet(rows, { uniqueHeaderRowNames, - fieldTypes: colFieldTypes, + columnConfigs, ignoredFieldColumnIndexes: ignoredColumns.map(col => uniqueHeaderRowNames.indexOf(col)), slugFieldColumnIndex: slugColumn ? uniqueHeaderRowNames.indexOf(slugColumn) : -1, + imageAltColumnIndexes, columnCount: headerRow.length, }) @@ -712,6 +769,7 @@ export async function syncSheet({ collection.setPluginData(PLUGIN_IGNORED_COLUMNS_KEY, JSON.stringify(ignoredColumns)), collection.setPluginData(PLUGIN_SHEET_HEADER_ROW_HASH_KEY, headerRowHash), collection.setPluginData(PLUGIN_SLUG_COLUMN_KEY, slugColumn), + collection.setPluginData(PLUGIN_ALT_TEXT_ASSIGNMENTS_KEY, JSON.stringify(altTextAssignments)), collection.setPluginData(PLUGIN_LAST_SYNCED_KEY, new Date().toISOString()), ]) @@ -747,12 +805,20 @@ export async function getPluginContext(): Promise { } // Fetch both new and legacy data - const [rawIgnoredColumns, rawSheetHeaderRowHash, storedSlugColumn, lastSyncedTime] = await Promise.all([ - collection.getPluginData(PLUGIN_IGNORED_COLUMNS_KEY), - collection.getPluginData(PLUGIN_SHEET_HEADER_ROW_HASH_KEY), - collection.getPluginData(PLUGIN_SLUG_COLUMN_KEY), - collection.getPluginData(PLUGIN_LAST_SYNCED_KEY), - ]) + const [rawIgnoredColumns, rawSheetHeaderRowHash, storedSlugColumn, lastSyncedTime, rawAltTextAssignments] = + await Promise.all([ + collection.getPluginData(PLUGIN_IGNORED_COLUMNS_KEY), + collection.getPluginData(PLUGIN_SHEET_HEADER_ROW_HASH_KEY), + collection.getPluginData(PLUGIN_SLUG_COLUMN_KEY), + collection.getPluginData(PLUGIN_LAST_SYNCED_KEY), + collection.getPluginData(PLUGIN_ALT_TEXT_ASSIGNMENTS_KEY), + ]) + + const altTextAssignmentsResult = v.safeParse( + v.pipe(v.string(), v.parseJson(), v.record(v.string(), v.string())), + rawAltTextAssignments + ) + const altTextAssignments = altTextAssignmentsResult.success ? altTextAssignmentsResult.output : {} let spreadsheetInfo @@ -865,6 +931,7 @@ export async function getPluginContext(): Promise { collectionFields, sheetHeaderRow, ignoredColumns, + altTextAssignments, hasChangedFields: storedSheetHeaderRowHash !== currentSheetHeaderRowHash, } } From 69448206950ebd6d0cfb0a21f71798352e5c6d72 Mon Sep 17 00:00:00 2001 From: djohalo2 Date: Fri, 17 Jul 2026 10:01:21 +0200 Subject: [PATCH 2/7] chore: refactor field type menu button into component --- .../src/components/FieldTypeMenuButton.tsx | 112 +++++++++++++++++ .../src/pages/MapSheetFields.tsx | 117 ++---------------- 2 files changed, 122 insertions(+), 107 deletions(-) create mode 100644 plugins/google-sheets/src/components/FieldTypeMenuButton.tsx diff --git a/plugins/google-sheets/src/components/FieldTypeMenuButton.tsx b/plugins/google-sheets/src/components/FieldTypeMenuButton.tsx new file mode 100644 index 000000000..208121180 --- /dev/null +++ b/plugins/google-sheets/src/components/FieldTypeMenuButton.tsx @@ -0,0 +1,112 @@ +import cx from "classnames" +import { framer, type MenuItem, useIsAllowedTo } from "framer-plugin" +import type { SheetCollectionFieldInput, VirtualFieldType } from "../sheets" +import { syncMethods } from "../utils" +import { IconChevron, IconChevronDown } from "./Icons" + +interface FieldTypeOption { + type: VirtualFieldType + label: string +} + +const fieldTypeOptions: FieldTypeOption[] = [ + { type: "string", label: "Plain Text" }, + { type: "formattedText", label: "Formatted Text" }, + { type: "date", label: "Date" }, + { type: "dateTime", label: "Date & Time" }, + { type: "link", label: "Link" }, + { type: "image", label: "Image" }, + { type: "color", label: "Color" }, + { type: "boolean", label: "Toggle" }, + { type: "number", label: "Number" }, + { type: "enum", label: "Option" }, + { type: "file", label: "File" }, +] + +const contextMenuOffset = 4 + +const getFieldTypeLabel = (field: SheetCollectionFieldInput, allFields: SheetCollectionFieldInput[]): string => { + if (field.type === "altText") { + const imageField = allFields.find(candidate => candidate.id === field.imageFieldId) + return imageField ? `Alt Text → ${imageField.name}` : "Alt Text" + } + + return fieldTypeOptions.find(option => option.type === field.type)?.label ?? field.type +} + +interface Props { + field: SheetCollectionFieldInput + fields: SheetCollectionFieldInput[] + disabled: boolean + disabledFieldIds: Set + onFieldTypeChange: (id: string, type: VirtualFieldType, imageFieldId?: string) => void +} + +export function FieldTypeMenuButton({ field, fields, disabled, disabledFieldIds, onFieldTypeChange }: Props) { + const isAllowedToManage = useIsAllowedTo("ManagedCollection.setFields", ...syncMethods) + const isDisabled = disabled || !isAllowedToManage + const imageField = fields.find(candidate => candidate.id === field.imageFieldId) + + const handleMenuOpen = (event: React.MouseEvent) => { + const availableImageFields = fields.filter( + candidate => candidate.type === "image" && candidate.id !== field.id && !disabledFieldIds.has(candidate.id) + ) + const { left, bottom, width } = event.currentTarget.getBoundingClientRect() + + const items: MenuItem[] = [ + ...fieldTypeOptions.map(({ type, label }) => ({ + label, + checked: field.type === type, + onAction: () => { + onFieldTypeChange(field.id, type) + }, + })), + { + label: "Alt Text", + checked: field.type === "altText", + enabled: availableImageFields.length > 0, + submenu: availableImageFields.map(candidate => ({ + label: candidate.name, + checked: field.type === "altText" && field.imageFieldId === candidate.id, + onAction: () => { + onFieldTypeChange(field.id, "altText", candidate.id) + }, + })), + }, + ] + + void framer.showContextMenu(items, { + location: { + x: left + width - contextMenuOffset, + y: bottom + contextMenuOffset, + }, + placement: "bottom-left", + width, + }) + } + + return ( + + ) +} diff --git a/plugins/google-sheets/src/pages/MapSheetFields.tsx b/plugins/google-sheets/src/pages/MapSheetFields.tsx index 0aaa43376..3af4018fd 100644 --- a/plugins/google-sheets/src/pages/MapSheetFields.tsx +++ b/plugins/google-sheets/src/pages/MapSheetFields.tsx @@ -1,8 +1,9 @@ import cx from "classnames" -import { framer, type MenuItem, useIsAllowedTo } from "framer-plugin" +import { framer, useIsAllowedTo } from "framer-plugin" import { Fragment, useMemo, useState } from "react" import { CheckboxTextfield } from "../components/CheckboxTextField" -import { IconChevron, IconChevronDown } from "../components/Icons" +import { FieldTypeMenuButton } from "../components/FieldTypeMenuButton" +import { IconChevron } from "../components/Icons" import type { CellValue, HeaderRow, @@ -15,25 +16,6 @@ import type { } from "../sheets" import { generateUniqueNames, isDefined, syncMethods } from "../utils" -interface FieldTypeOption { - type: VirtualFieldType - label: string -} - -const fieldTypeOptions: FieldTypeOption[] = [ - { type: "string", label: "Plain Text" }, - { type: "formattedText", label: "Formatted Text" }, - { type: "date", label: "Date" }, - { type: "dateTime", label: "Date & Time" }, - { type: "link", label: "Link" }, - { type: "image", label: "Image" }, - { type: "color", label: "Color" }, - { type: "boolean", label: "Toggle" }, - { type: "number", label: "Number" }, - { type: "enum", label: "Option" }, - { type: "file", label: "File" }, -] - const getInitialSlugColumn = (context: PluginContext, slugFields: SheetCollectionFieldInput[]): string => { if (context.type === "update" && context.slugColumn) { return context.slugColumn @@ -157,19 +139,6 @@ const getPossibleSlugFields = (fieldConfig: SheetCollectionFieldInput[]): SheetC return fieldConfig.filter(field => field.type === "string") } -const getFieldTypeLabel = (field: SheetCollectionFieldInput, allFields: SheetCollectionFieldInput[]): string => { - if (field.type === "altText") { - const imageField = allFields.find(f => f.id === field.imageFieldId) - return imageField ? `Alt Text → ${imageField.name}` : "Alt Text" - } - - return fieldTypeOptions.find(option => option.type === field.type)?.label ?? field.type -} - -const getAltTextImageField = (field: SheetCollectionFieldInput, allFields: SheetCollectionFieldInput[]) => { - return allFields.find(candidate => candidate.id === field.imageFieldId) -} - interface Props { spreadsheetId: string sheetTitle: string @@ -236,44 +205,6 @@ export function MapSheetFieldsPage({ ) } - const handleFieldTypeMenuOpen = (field: SheetCollectionFieldInput, e: React.MouseEvent) => { - const imageFields = fieldConfig.filter( - f => f.type === "image" && f.id !== field.id && !disabledColumns.has(f.id) - ) - const { left, bottom, width } = e.currentTarget.getBoundingClientRect() - - const items: MenuItem[] = [ - ...fieldTypeOptions.map(({ type, label }) => ({ - label, - checked: field.type === type, - onAction: () => { - handleFieldTypeChange(field.id, type) - }, - })), - { - label: "Alt Text", - checked: field.type === "altText", - enabled: imageFields.length > 0, - submenu: imageFields.map(imgField => ({ - label: imgField.name, - checked: field.type === "altText" && field.imageFieldId === imgField.id, - onAction: () => { - handleFieldTypeChange(field.id, "altText", imgField.id) - }, - })), - }, - ] - - const locationX = left + width - 4; - const locationY = bottom + 4; - - void framer.showContextMenu(items, { - location: { x: locationX, y: locationY }, - placement: "bottom-left", - width, - }) - } - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -351,41 +282,13 @@ export function MapSheetFieldsPage({
- + {field.type !== "altText" ? ( Date: Fri, 17 Jul 2026 10:37:16 +0200 Subject: [PATCH 3/7] chore: extract sheets image id index function --- plugins/google-sheets/src/App.tsx | 2 +- .../src/pages/MapSheetFields.tsx | 31 ++++++----- plugins/google-sheets/src/sheets.ts | 53 ++++++++++++------- 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/plugins/google-sheets/src/App.tsx b/plugins/google-sheets/src/App.tsx index 7a842113a..d12ef5811 100644 --- a/plugins/google-sheets/src/App.tsx +++ b/plugins/google-sheets/src/App.tsx @@ -229,7 +229,7 @@ export function App({ pluginContext }: AppProps) { // Determine if the field type is already configured, otherwise default to "string". // Alt text columns never become real CMS fields, so `fields` won't have them. columnConfigs: uniqueHeaderRowNames.map(columnId => { - if (columnId in altTextAssignments) { + if (Object.hasOwn(altTextAssignments, columnId)) { return { type: "altText" as const, imageFieldId: altTextAssignments[columnId] } } const field = fields.find(field => field.id === columnId) diff --git a/plugins/google-sheets/src/pages/MapSheetFields.tsx b/plugins/google-sheets/src/pages/MapSheetFields.tsx index 3af4018fd..74c4340aa 100644 --- a/plugins/google-sheets/src/pages/MapSheetFields.tsx +++ b/plugins/google-sheets/src/pages/MapSheetFields.tsx @@ -87,7 +87,7 @@ const inferFieldType = (cellValue: CellValue): VirtualFieldType => { } const getColumnConfig = (context: PluginContext, columnId: string, cellValue?: CellValue): SheetColumnConfig => { - if (context.type === "update" && columnId in context.altTextAssignments) { + if (context.type === "update" && Object.hasOwn(context.altTextAssignments, columnId)) { return { type: "altText", imageFieldId: context.altTextAssignments[columnId] } } @@ -191,18 +191,23 @@ export function MapSheetFieldsPage({ } const handleFieldTypeChange = (id: string, type: VirtualFieldType, imageFieldId?: string) => { - setFieldConfig(current => - current.map(field => { - if (field.id === id) { - return { - ...field, - type, - imageFieldId: type === "altText" ? imageFieldId : undefined, - } as SheetCollectionFieldInput - } - return field - }) - ) + const nextFieldConfig = fieldConfig.map(field => { + if (field.id === id) { + return { + ...field, + type, + imageFieldId: type === "altText" ? imageFieldId : undefined, + } as SheetCollectionFieldInput + } + return field + }) + + setFieldConfig(nextFieldConfig) + + const nextSlugFields = getPossibleSlugFields(nextFieldConfig) + if (!nextSlugFields.some(field => field.id === slugColumn)) { + setSlugColumn(nextSlugFields[0]?.id ?? "") + } } const handleSubmit = async (e: React.FormEvent) => { diff --git a/plugins/google-sheets/src/sheets.ts b/plugins/google-sheets/src/sheets.ts index 70ccee268..5bcce2ca0 100644 --- a/plugins/google-sheets/src/sheets.ts +++ b/plugins/google-sheets/src/sheets.ts @@ -311,7 +311,7 @@ interface ProcessSheetRowParams { uniqueHeaderRowNames: string[] slugFieldColumnIndex: number ignoredFieldColumnIndexes: number[] - imageAltColumnIndexes: Map + altColumnIndexByImageColumnIndex: Map status: SyncStatus } @@ -419,7 +419,7 @@ function enrichFieldsWithEnumCases( }) } -function getAltTextAssignments(columnConfigs: SheetColumnConfig[], uniqueHeaderRowNames: string[]) { +function buildAltTextAssignments(columnConfigs: SheetColumnConfig[], uniqueHeaderRowNames: string[]) { return columnConfigs.reduce>((assignments, column, i) => { if (column.type !== "altText") return assignments @@ -431,6 +431,29 @@ function getAltTextAssignments(columnConfigs: SheetColumnConfig[], uniqueHeaderR }, {}) } +function buildAltColumnIndexByImageColumnIndex( + columnConfigs: SheetColumnConfig[], + uniqueHeaderRowNames: string[], + ignoredColumns: string[] +): Map { + const altColumnIndexByImageColumnIndex = new Map() + + for (let i = 0; i < columnConfigs.length; i++) { + if (columnConfigs[i]?.type !== "altText") continue + if (ignoredColumns.includes(uniqueHeaderRowNames[i] ?? "")) continue + + const imageFieldId = columnConfigs[i]?.imageFieldId + if (!imageFieldId || ignoredColumns.includes(imageFieldId)) continue + + const imageColumnIndex = uniqueHeaderRowNames.indexOf(imageFieldId) + if (imageColumnIndex === -1 || columnConfigs[imageColumnIndex]?.type !== "image") continue + + altColumnIndexByImageColumnIndex.set(imageColumnIndex, i) + } + + return altColumnIndexByImageColumnIndex +} + function getFieldDataEntryInput( type: VirtualFieldType, cellValue: CellValue, @@ -499,7 +522,7 @@ function processSheetRow({ uniqueHeaderRowNames, ignoredFieldColumnIndexes, slugFieldColumnIndex, - imageAltColumnIndexes, + altColumnIndexByImageColumnIndex, status, columnConfigs, }: ProcessSheetRowParams) { @@ -522,7 +545,7 @@ function processSheetRow({ if (isIgnored && !isSlugField) continue - const altColumnIndex = imageAltColumnIndexes.get(i) + const altColumnIndex = altColumnIndexByImageColumnIndex.get(i) const altCell = isDefined(altColumnIndex) ? (row[altColumnIndex] ?? null) : undefined let fieldDataEntryInput = getFieldDataEntryInput(fieldType, cell, altCell) @@ -689,7 +712,7 @@ export async function syncSheet({ const uniqueHeaderRowNames = generateUniqueNames(headerRow) - const altTextAssignments = getAltTextAssignments(columnConfigs, uniqueHeaderRowNames) + const altTextAssignments = buildAltTextAssignments(columnConfigs, uniqueHeaderRowNames) const enrichedFields = enrichFieldsWithEnumCases(fields, uniqueHeaderRowNames, rows) const needsFieldSchemaUpdate = configureFields || enrichedFields.some(field => field.type === "enum") @@ -722,26 +745,18 @@ export async function syncSheet({ ) } - const imageAltColumnIndexes = new Map() - for (let i = 0; i < columnConfigs.length; i++) { - if (columnConfigs[i]?.type !== "altText") continue - if (ignoredColumns.includes(uniqueHeaderRowNames[i] ?? "")) continue - - const imageFieldId = columnConfigs[i]?.imageFieldId - if (!imageFieldId || ignoredColumns.includes(imageFieldId)) continue - - const imageColIndex = uniqueHeaderRowNames.indexOf(imageFieldId) - if (imageColIndex === -1 || columnConfigs[imageColIndex]?.type !== "image") continue - - imageAltColumnIndexes.set(imageColIndex, i) - } + const altColumnIndexByImageColumnIndex = buildAltColumnIndexByImageColumnIndex( + columnConfigs, + uniqueHeaderRowNames, + ignoredColumns + ) const { collectionItems, status } = processSheet(rows, { uniqueHeaderRowNames, columnConfigs, ignoredFieldColumnIndexes: ignoredColumns.map(col => uniqueHeaderRowNames.indexOf(col)), slugFieldColumnIndex: slugColumn ? uniqueHeaderRowNames.indexOf(slugColumn) : -1, - imageAltColumnIndexes, + altColumnIndexByImageColumnIndex, columnCount: headerRow.length, }) From 281f01971121e9b6035cb180419fd3dd19d5db40 Mon Sep 17 00:00:00 2001 From: djohalo2 Date: Fri, 17 Jul 2026 11:01:36 +0200 Subject: [PATCH 4/7] chore: cleanup --- ...enuButton.tsx => FieldTypeSelectField.tsx} | 21 +++++----- .../src/pages/MapSheetFields.tsx | 40 +++++++++---------- plugins/google-sheets/src/sheets.ts | 18 +++++---- 3 files changed, 37 insertions(+), 42 deletions(-) rename plugins/google-sheets/src/components/{FieldTypeMenuButton.tsx => FieldTypeSelectField.tsx} (81%) diff --git a/plugins/google-sheets/src/components/FieldTypeMenuButton.tsx b/plugins/google-sheets/src/components/FieldTypeSelectField.tsx similarity index 81% rename from plugins/google-sheets/src/components/FieldTypeMenuButton.tsx rename to plugins/google-sheets/src/components/FieldTypeSelectField.tsx index 208121180..09adc1ed0 100644 --- a/plugins/google-sheets/src/components/FieldTypeMenuButton.tsx +++ b/plugins/google-sheets/src/components/FieldTypeSelectField.tsx @@ -1,7 +1,6 @@ import cx from "classnames" -import { framer, type MenuItem, useIsAllowedTo } from "framer-plugin" -import type { SheetCollectionFieldInput, VirtualFieldType } from "../sheets" -import { syncMethods } from "../utils" +import { framer, type MenuItem } from "framer-plugin" +import { isAltTextColumn, type SheetCollectionFieldInput, type VirtualFieldType } from "../sheets" import { IconChevron, IconChevronDown } from "./Icons" interface FieldTypeOption { @@ -26,7 +25,7 @@ const fieldTypeOptions: FieldTypeOption[] = [ const contextMenuOffset = 4 const getFieldTypeLabel = (field: SheetCollectionFieldInput, allFields: SheetCollectionFieldInput[]): string => { - if (field.type === "altText") { + if (isAltTextColumn(field)) { const imageField = allFields.find(candidate => candidate.id === field.imageFieldId) return imageField ? `Alt Text → ${imageField.name}` : "Alt Text" } @@ -37,14 +36,12 @@ const getFieldTypeLabel = (field: SheetCollectionFieldInput, allFields: SheetCol interface Props { field: SheetCollectionFieldInput fields: SheetCollectionFieldInput[] - disabled: boolean + isDisabled: boolean disabledFieldIds: Set onFieldTypeChange: (id: string, type: VirtualFieldType, imageFieldId?: string) => void } -export function FieldTypeMenuButton({ field, fields, disabled, disabledFieldIds, onFieldTypeChange }: Props) { - const isAllowedToManage = useIsAllowedTo("ManagedCollection.setFields", ...syncMethods) - const isDisabled = disabled || !isAllowedToManage +export function FieldTypeSelectField({ field, fields, isDisabled, disabledFieldIds, onFieldTypeChange }: Props) { const imageField = fields.find(candidate => candidate.id === field.imageFieldId) const handleMenuOpen = (event: React.MouseEvent) => { @@ -63,11 +60,11 @@ export function FieldTypeMenuButton({ field, fields, disabled, disabledFieldIds, })), { label: "Alt Text", - checked: field.type === "altText", + checked: isAltTextColumn(field), enabled: availableImageFields.length > 0, submenu: availableImageFields.map(candidate => ({ label: candidate.name, - checked: field.type === "altText" && field.imageFieldId === candidate.id, + checked: isAltTextColumn(field) && field.imageFieldId === candidate.id, onAction: () => { onFieldTypeChange(field.id, "altText", candidate.id) }, @@ -90,10 +87,10 @@ export function FieldTypeMenuButton({ field, fields, disabled, disabledFieldIds, type="button" className={cx("flex w-full min-w-0 items-center gap-1 text-left", isDisabled && "opacity-50")} disabled={isDisabled} - title={isAllowedToManage ? getFieldTypeLabel(field, fields) : "Insufficient permissions"} + title={getFieldTypeLabel(field, fields)} onClick={handleMenuOpen} > - {field.type === "altText" ? ( + {isAltTextColumn(field) ? ( Alt Text diff --git a/plugins/google-sheets/src/pages/MapSheetFields.tsx b/plugins/google-sheets/src/pages/MapSheetFields.tsx index 74c4340aa..74b5928a5 100644 --- a/plugins/google-sheets/src/pages/MapSheetFields.tsx +++ b/plugins/google-sheets/src/pages/MapSheetFields.tsx @@ -2,7 +2,7 @@ import cx from "classnames" import { framer, useIsAllowedTo } from "framer-plugin" import { Fragment, useMemo, useState } from "react" import { CheckboxTextfield } from "../components/CheckboxTextField" -import { FieldTypeMenuButton } from "../components/FieldTypeMenuButton" +import { FieldTypeSelectField } from "../components/FieldTypeSelectField" import { IconChevron } from "../components/Icons" import type { CellValue, @@ -14,6 +14,7 @@ import type { SyncMutationOptions, VirtualFieldType, } from "../sheets" +import { isAltTextColumn } from "../sheets" import { generateUniqueNames, isDefined, syncMethods } from "../utils" const getInitialSlugColumn = (context: PluginContext, slugFields: SheetCollectionFieldInput[]): string => { @@ -191,23 +192,18 @@ export function MapSheetFieldsPage({ } const handleFieldTypeChange = (id: string, type: VirtualFieldType, imageFieldId?: string) => { - const nextFieldConfig = fieldConfig.map(field => { - if (field.id === id) { - return { - ...field, - type, - imageFieldId: type === "altText" ? imageFieldId : undefined, - } as SheetCollectionFieldInput - } - return field - }) - - setFieldConfig(nextFieldConfig) - - const nextSlugFields = getPossibleSlugFields(nextFieldConfig) - if (!nextSlugFields.some(field => field.id === slugColumn)) { - setSlugColumn(nextSlugFields[0]?.id ?? "") - } + setFieldConfig(current => + current.map(field => { + if (field.id === id) { + return { + ...field, + type, + imageFieldId: type === "altText" ? imageFieldId : undefined, + } as SheetCollectionFieldInput + } + return field + }) + ) } const handleSubmit = async (e: React.FormEvent) => { @@ -217,7 +213,7 @@ export function MapSheetFieldsPage({ const allFields = fieldConfig // Alt text columns are virtual: they never become their own CMS field. - .filter(field => !disabledColumns.has(field.id) && field.type !== "altText") + .filter(field => !disabledColumns.has(field.id) && !isAltTextColumn(field)) .map(field => { const maybeOverride = fieldNameOverrides[field.id] if (maybeOverride) { @@ -287,14 +283,14 @@ export function MapSheetFieldsPage({
- - {field.type !== "altText" ? ( + {!isAltTextColumn(field) ? ( ): boolean => column.type === "altText" + export interface PluginContextNew { type: "new" collection: ManagedCollection @@ -421,7 +423,7 @@ function enrichFieldsWithEnumCases( function buildAltTextAssignments(columnConfigs: SheetColumnConfig[], uniqueHeaderRowNames: string[]) { return columnConfigs.reduce>((assignments, column, i) => { - if (column.type !== "altText") return assignments + if (!isAltTextColumn(column)) return assignments const imageFieldId = column.imageFieldId const altColumnId = uniqueHeaderRowNames[i] @@ -439,10 +441,11 @@ function buildAltColumnIndexByImageColumnIndex( const altColumnIndexByImageColumnIndex = new Map() for (let i = 0; i < columnConfigs.length; i++) { - if (columnConfigs[i]?.type !== "altText") continue + const columnConfig = columnConfigs[i] + if (!columnConfig || !isAltTextColumn(columnConfig)) continue if (ignoredColumns.includes(uniqueHeaderRowNames[i] ?? "")) continue - const imageFieldId = columnConfigs[i]?.imageFieldId + const imageFieldId = columnConfig.imageFieldId if (!imageFieldId || ignoredColumns.includes(imageFieldId)) continue const imageColumnIndex = uniqueHeaderRowNames.indexOf(imageFieldId) @@ -533,11 +536,10 @@ function processSheetRow({ for (let i = 0; i < columnCount; i++) { const cell = row[i] ?? null - const fieldType = columnConfigs[i]?.type - if (!fieldType) continue - - // Alt text columns are virtual and never become their own field - if (fieldType === "altText") continue + const columnConfig = columnConfigs[i] + // Alt text columns are virtual and never become their own field. + if (!columnConfig || isAltTextColumn(columnConfig)) continue + const fieldType = columnConfig.type // Skip processing ignored columns unless they are the slug field const isIgnored = ignoredFieldColumnIndexes.includes(i) From 262b3a9dc81fe611f96d40566bafa253ae657695 Mon Sep 17 00:00:00 2001 From: djohalo2 Date: Fri, 17 Jul 2026 11:28:16 +0200 Subject: [PATCH 5/7] fix: gracefully handle changing image field type with alt text pointer --- plugins/google-sheets/src/App.tsx | 5 ++--- plugins/google-sheets/src/pages/MapSheetFields.tsx | 11 ++++++++++- plugins/google-sheets/src/sheets.ts | 3 +++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/plugins/google-sheets/src/App.tsx b/plugins/google-sheets/src/App.tsx index d12ef5811..2fe8f184e 100644 --- a/plugins/google-sheets/src/App.tsx +++ b/plugins/google-sheets/src/App.tsx @@ -15,7 +15,7 @@ import { useSyncSheetMutation, } from "./sheets" import { showFieldMappingUI, showLoginUI } from "./ui" -import { assert, generateUniqueNames, syncMethods } from "./utils" +import { assert, syncMethods } from "./utils" interface AppProps { pluginContext: PluginContext @@ -212,7 +212,6 @@ export function App({ pluginContext }: AppProps) { altTextAssignments, } = context const [headerRow] = sheet.values - const uniqueHeaderRowNames = generateUniqueNames(headerRow) const task = async () => { try { @@ -228,7 +227,7 @@ export function App({ pluginContext }: AppProps) { fields, // Determine if the field type is already configured, otherwise default to "string". // Alt text columns never become real CMS fields, so `fields` won't have them. - columnConfigs: uniqueHeaderRowNames.map(columnId => { + columnConfigs: headerRow.map(columnId => { if (Object.hasOwn(altTextAssignments, columnId)) { return { type: "altText" as const, imageFieldId: altTextAssignments[columnId] } } diff --git a/plugins/google-sheets/src/pages/MapSheetFields.tsx b/plugins/google-sheets/src/pages/MapSheetFields.tsx index 74b5928a5..e1aab6177 100644 --- a/plugins/google-sheets/src/pages/MapSheetFields.tsx +++ b/plugins/google-sheets/src/pages/MapSheetFields.tsx @@ -199,8 +199,17 @@ export function MapSheetFieldsPage({ ...field, type, imageFieldId: type === "altText" ? imageFieldId : undefined, - } as SheetCollectionFieldInput + } } + + if (type !== "image" && isAltTextColumn(field) && field.imageFieldId === id) { + return { + ...field, + type: "string", + imageFieldId: undefined, + } + } + return field }) ) diff --git a/plugins/google-sheets/src/sheets.ts b/plugins/google-sheets/src/sheets.ts index 7e3355aca..09d017dbd 100644 --- a/plugins/google-sheets/src/sheets.ts +++ b/plugins/google-sheets/src/sheets.ts @@ -429,6 +429,9 @@ function buildAltTextAssignments(columnConfigs: SheetColumnConfig[], uniqueHeade const altColumnId = uniqueHeaderRowNames[i] if (!imageFieldId || !altColumnId) return assignments + const imageColumnIndex = uniqueHeaderRowNames.indexOf(imageFieldId) + if (imageColumnIndex === -1 || columnConfigs[imageColumnIndex]?.type !== "image") return assignments + return { ...assignments, [altColumnId]: imageFieldId } }, {}) } From d9a015b9e389b07c85788752bd466ad0dfa82aa3 Mon Sep 17 00:00:00 2001 From: djohalo2 Date: Fri, 17 Jul 2026 15:57:20 +0200 Subject: [PATCH 6/7] chore: cleanup types --- .../src/components/FieldTypeSelectField.tsx | 24 ++++++------ plugins/google-sheets/src/sheets.ts | 37 ++++++++++--------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/plugins/google-sheets/src/components/FieldTypeSelectField.tsx b/plugins/google-sheets/src/components/FieldTypeSelectField.tsx index 09adc1ed0..2d817d328 100644 --- a/plugins/google-sheets/src/components/FieldTypeSelectField.tsx +++ b/plugins/google-sheets/src/components/FieldTypeSelectField.tsx @@ -1,5 +1,6 @@ import cx from "classnames" import { framer, type MenuItem } from "framer-plugin" +import { useMemo } from "react" import { isAltTextColumn, type SheetCollectionFieldInput, type VirtualFieldType } from "../sheets" import { IconChevron, IconChevronDown } from "./Icons" @@ -24,15 +25,6 @@ const fieldTypeOptions: FieldTypeOption[] = [ const contextMenuOffset = 4 -const getFieldTypeLabel = (field: SheetCollectionFieldInput, allFields: SheetCollectionFieldInput[]): string => { - if (isAltTextColumn(field)) { - const imageField = allFields.find(candidate => candidate.id === field.imageFieldId) - return imageField ? `Alt Text → ${imageField.name}` : "Alt Text" - } - - return fieldTypeOptions.find(option => option.type === field.type)?.label ?? field.type -} - interface Props { field: SheetCollectionFieldInput fields: SheetCollectionFieldInput[] @@ -44,11 +36,18 @@ interface Props { export function FieldTypeSelectField({ field, fields, isDisabled, disabledFieldIds, onFieldTypeChange }: Props) { const imageField = fields.find(candidate => candidate.id === field.imageFieldId) + const fieldTypeLabel = useMemo(() => { + if (isAltTextColumn(field)) { + return imageField ? `Alt Text → ${imageField.name}` : "Alt Text" + } + + return fieldTypeOptions.find(option => option.type === field.type)?.label ?? field.type + }, [field, imageField]) + const handleMenuOpen = (event: React.MouseEvent) => { const availableImageFields = fields.filter( candidate => candidate.type === "image" && candidate.id !== field.id && !disabledFieldIds.has(candidate.id) ) - const { left, bottom, width } = event.currentTarget.getBoundingClientRect() const items: MenuItem[] = [ ...fieldTypeOptions.map(({ type, label }) => ({ @@ -72,6 +71,7 @@ export function FieldTypeSelectField({ field, fields, isDisabled, disabledFieldI }, ] + const { left, bottom, width } = event.currentTarget.getBoundingClientRect() void framer.showContextMenu(items, { location: { x: left + width - contextMenuOffset, @@ -87,7 +87,7 @@ export function FieldTypeSelectField({ field, fields, isDisabled, disabledFieldI type="button" className={cx("flex w-full min-w-0 items-center gap-1 text-left", isDisabled && "opacity-50")} disabled={isDisabled} - title={getFieldTypeLabel(field, fields)} + title={fieldTypeLabel} onClick={handleMenuOpen} > {isAltTextColumn(field) ? ( @@ -99,7 +99,7 @@ export function FieldTypeSelectField({ field, fields, isDisabled, disabledFieldI {imageField?.name ?? "Image"}
) : ( - {getFieldTypeLabel(field, fields)} + {fieldTypeLabel} )} diff --git a/plugins/google-sheets/src/sheets.ts b/plugins/google-sheets/src/sheets.ts index 09d017dbd..fb7b689b1 100644 --- a/plugins/google-sheets/src/sheets.ts +++ b/plugins/google-sheets/src/sheets.ts @@ -237,16 +237,14 @@ function fetchSheetWithClient(spreadsheetId: string, sheetTitle: string, range?: export type CollectionFieldType = ManagedCollectionFieldInput["type"] export type VirtualFieldType = CollectionFieldType | "dateTime" | "altText" -export type SheetCollectionFieldInput = Omit & { - type: VirtualFieldType - imageFieldId?: string -} export interface SheetColumnConfig { type: VirtualFieldType imageFieldId?: string } +export type SheetCollectionFieldInput = Omit & SheetColumnConfig + export const isAltTextColumn = (column: Pick): boolean => column.type === "altText" export interface PluginContextNew { @@ -421,19 +419,23 @@ function enrichFieldsWithEnumCases( }) } -function buildAltTextAssignments(columnConfigs: SheetColumnConfig[], uniqueHeaderRowNames: string[]) { - return columnConfigs.reduce>((assignments, column, i) => { - if (!isAltTextColumn(column)) return assignments +function buildAltTextAssignments(columnConfigs: SheetColumnConfig[], uniqueHeaderRowNames: string[]): Record { + const assignments: Record = {} + + for (const [altColumnIndex, columnConfig] of columnConfigs.entries()) { + if (!isAltTextColumn(columnConfig)) continue - const imageFieldId = column.imageFieldId - const altColumnId = uniqueHeaderRowNames[i] - if (!imageFieldId || !altColumnId) return assignments + const altColumnId = uniqueHeaderRowNames[altColumnIndex] + const imageFieldId = columnConfig.imageFieldId + if (!altColumnId || !imageFieldId) continue const imageColumnIndex = uniqueHeaderRowNames.indexOf(imageFieldId) - if (imageColumnIndex === -1 || columnConfigs[imageColumnIndex]?.type !== "image") return assignments + if (imageColumnIndex === -1 || columnConfigs[imageColumnIndex]?.type !== "image") continue - return { ...assignments, [altColumnId]: imageFieldId } - }, {}) + assignments[altColumnId] = imageFieldId + } + + return assignments } function buildAltColumnIndexByImageColumnIndex( @@ -443,10 +445,9 @@ function buildAltColumnIndexByImageColumnIndex( ): Map { const altColumnIndexByImageColumnIndex = new Map() - for (let i = 0; i < columnConfigs.length; i++) { - const columnConfig = columnConfigs[i] - if (!columnConfig || !isAltTextColumn(columnConfig)) continue - if (ignoredColumns.includes(uniqueHeaderRowNames[i] ?? "")) continue + for (const [altColumnIndex, columnConfig] of columnConfigs.entries()) { + const altColumnId = uniqueHeaderRowNames[altColumnIndex] + if (!altColumnId || ignoredColumns.includes(altColumnId) || !isAltTextColumn(columnConfig)) continue const imageFieldId = columnConfig.imageFieldId if (!imageFieldId || ignoredColumns.includes(imageFieldId)) continue @@ -454,7 +455,7 @@ function buildAltColumnIndexByImageColumnIndex( const imageColumnIndex = uniqueHeaderRowNames.indexOf(imageFieldId) if (imageColumnIndex === -1 || columnConfigs[imageColumnIndex]?.type !== "image") continue - altColumnIndexByImageColumnIndex.set(imageColumnIndex, i) + altColumnIndexByImageColumnIndex.set(imageColumnIndex, altColumnIndex) } return altColumnIndexByImageColumnIndex From 9558707a0629dc05ede57c110f9dcd1bf4c022d1 Mon Sep 17 00:00:00 2001 From: djohalo2 Date: Fri, 17 Jul 2026 16:06:36 +0200 Subject: [PATCH 7/7] chore: rename imageColumnId --- plugins/google-sheets/src/App.tsx | 2 +- .../src/components/FieldTypeSelectField.tsx | 12 +++++------ .../src/pages/MapSheetFields.tsx | 16 +++++++------- plugins/google-sheets/src/sheets.ts | 21 +++++++++++-------- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/plugins/google-sheets/src/App.tsx b/plugins/google-sheets/src/App.tsx index 2fe8f184e..53b3c885e 100644 --- a/plugins/google-sheets/src/App.tsx +++ b/plugins/google-sheets/src/App.tsx @@ -229,7 +229,7 @@ export function App({ pluginContext }: AppProps) { // Alt text columns never become real CMS fields, so `fields` won't have them. columnConfigs: headerRow.map(columnId => { if (Object.hasOwn(altTextAssignments, columnId)) { - return { type: "altText" as const, imageFieldId: altTextAssignments[columnId] } + return { type: "altText" as const, imageColumnId: altTextAssignments[columnId] } } const field = fields.find(field => field.id === columnId) return { type: field?.type ?? "string" } diff --git a/plugins/google-sheets/src/components/FieldTypeSelectField.tsx b/plugins/google-sheets/src/components/FieldTypeSelectField.tsx index 2d817d328..181171fd4 100644 --- a/plugins/google-sheets/src/components/FieldTypeSelectField.tsx +++ b/plugins/google-sheets/src/components/FieldTypeSelectField.tsx @@ -30,19 +30,19 @@ interface Props { fields: SheetCollectionFieldInput[] isDisabled: boolean disabledFieldIds: Set - onFieldTypeChange: (id: string, type: VirtualFieldType, imageFieldId?: string) => void + onFieldTypeChange: (id: string, type: VirtualFieldType, imageColumnId?: string) => void } export function FieldTypeSelectField({ field, fields, isDisabled, disabledFieldIds, onFieldTypeChange }: Props) { - const imageField = fields.find(candidate => candidate.id === field.imageFieldId) + const imageColumn = fields.find(candidate => candidate.id === field.imageColumnId) const fieldTypeLabel = useMemo(() => { if (isAltTextColumn(field)) { - return imageField ? `Alt Text → ${imageField.name}` : "Alt Text" + return imageColumn ? `Alt Text → ${imageColumn.name}` : "Alt Text" } return fieldTypeOptions.find(option => option.type === field.type)?.label ?? field.type - }, [field, imageField]) + }, [field, imageColumn]) const handleMenuOpen = (event: React.MouseEvent) => { const availableImageFields = fields.filter( @@ -63,7 +63,7 @@ export function FieldTypeSelectField({ field, fields, isDisabled, disabledFieldI enabled: availableImageFields.length > 0, submenu: availableImageFields.map(candidate => ({ label: candidate.name, - checked: isAltTextColumn(field) && field.imageFieldId === candidate.id, + checked: isAltTextColumn(field) && field.imageColumnId === candidate.id, onAction: () => { onFieldTypeChange(field.id, "altText", candidate.id) }, @@ -96,7 +96,7 @@ export function FieldTypeSelectField({ field, fields, isDisabled, disabledFieldI Alt Text - {imageField?.name ?? "Image"} + {imageColumn?.name ?? "Image"}
) : ( {fieldTypeLabel} diff --git a/plugins/google-sheets/src/pages/MapSheetFields.tsx b/plugins/google-sheets/src/pages/MapSheetFields.tsx index e1aab6177..69f3e12fa 100644 --- a/plugins/google-sheets/src/pages/MapSheetFields.tsx +++ b/plugins/google-sheets/src/pages/MapSheetFields.tsx @@ -89,7 +89,7 @@ const inferFieldType = (cellValue: CellValue): VirtualFieldType => { const getColumnConfig = (context: PluginContext, columnId: string, cellValue?: CellValue): SheetColumnConfig => { if (context.type === "update" && Object.hasOwn(context.altTextAssignments, columnId)) { - return { type: "altText", imageFieldId: context.altTextAssignments[columnId] } + return { type: "altText", imageColumnId: context.altTextAssignments[columnId] } } // Determine if the field type is already configured @@ -113,13 +113,13 @@ const createFieldConfig = ( const sanitizedName = uniqueColumnNames[columnIndex] if (!sanitizedName) return null - const { type, imageFieldId } = getColumnConfig(context, sanitizedName, row?.[columnIndex]) + const { type, imageColumnId } = getColumnConfig(context, sanitizedName, row?.[columnIndex]) return { id: sanitizedName, name: sanitizedName, type, - imageFieldId, + imageColumnId, } as SheetCollectionFieldInput }) .filter(isDefined) @@ -191,22 +191,22 @@ export function MapSheetFieldsPage({ })) } - const handleFieldTypeChange = (id: string, type: VirtualFieldType, imageFieldId?: string) => { + const handleFieldTypeChange = (id: string, type: VirtualFieldType, imageColumnId?: string) => { setFieldConfig(current => current.map(field => { if (field.id === id) { return { ...field, type, - imageFieldId: type === "altText" ? imageFieldId : undefined, + imageColumnId: type === "altText" ? imageColumnId : undefined, } } - if (type !== "image" && isAltTextColumn(field) && field.imageFieldId === id) { + if (type !== "image" && isAltTextColumn(field) && field.imageColumnId === id) { return { ...field, type: "string", - imageFieldId: undefined, + imageColumnId: undefined, } } @@ -238,7 +238,7 @@ export function MapSheetFieldsPage({ fields: allFields, spreadsheetId, sheetTitle, - columnConfigs: fieldConfig.map(field => ({ type: field.type, imageFieldId: field.imageFieldId })), + columnConfigs: fieldConfig.map(field => ({ type: field.type, imageColumnId: field.imageColumnId })), ignoredColumns: Array.from(disabledColumns), slugColumn, lastSyncedTime: getLastSyncedTime(pluginContext, slugColumn), diff --git a/plugins/google-sheets/src/sheets.ts b/plugins/google-sheets/src/sheets.ts index fb7b689b1..2faa75583 100644 --- a/plugins/google-sheets/src/sheets.ts +++ b/plugins/google-sheets/src/sheets.ts @@ -240,7 +240,7 @@ export type VirtualFieldType = CollectionFieldType | "dateTime" | "altText" export interface SheetColumnConfig { type: VirtualFieldType - imageFieldId?: string + imageColumnId?: string } export type SheetCollectionFieldInput = Omit & SheetColumnConfig @@ -419,20 +419,23 @@ function enrichFieldsWithEnumCases( }) } -function buildAltTextAssignments(columnConfigs: SheetColumnConfig[], uniqueHeaderRowNames: string[]): Record { +function buildAltTextAssignments( + columnConfigs: SheetColumnConfig[], + uniqueHeaderRowNames: string[] +): Record { const assignments: Record = {} for (const [altColumnIndex, columnConfig] of columnConfigs.entries()) { if (!isAltTextColumn(columnConfig)) continue const altColumnId = uniqueHeaderRowNames[altColumnIndex] - const imageFieldId = columnConfig.imageFieldId - if (!altColumnId || !imageFieldId) continue + const imageColumnId = columnConfig.imageColumnId + if (!altColumnId || !imageColumnId) continue - const imageColumnIndex = uniqueHeaderRowNames.indexOf(imageFieldId) + const imageColumnIndex = uniqueHeaderRowNames.indexOf(imageColumnId) if (imageColumnIndex === -1 || columnConfigs[imageColumnIndex]?.type !== "image") continue - assignments[altColumnId] = imageFieldId + assignments[altColumnId] = imageColumnId } return assignments @@ -449,10 +452,10 @@ function buildAltColumnIndexByImageColumnIndex( const altColumnId = uniqueHeaderRowNames[altColumnIndex] if (!altColumnId || ignoredColumns.includes(altColumnId) || !isAltTextColumn(columnConfig)) continue - const imageFieldId = columnConfig.imageFieldId - if (!imageFieldId || ignoredColumns.includes(imageFieldId)) continue + const imageColumnId = columnConfig.imageColumnId + if (!imageColumnId || ignoredColumns.includes(imageColumnId)) continue - const imageColumnIndex = uniqueHeaderRowNames.indexOf(imageFieldId) + const imageColumnIndex = uniqueHeaderRowNames.indexOf(imageColumnId) if (imageColumnIndex === -1 || columnConfigs[imageColumnIndex]?.type !== "image") continue altColumnIndexByImageColumnIndex.set(imageColumnIndex, altColumnIndex)