Skip to content
Merged
13 changes: 9 additions & 4 deletions plugins/google-sheets/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export function App({ pluginContext }: AppProps) {
slugColumn,
lastSyncedTime,
sheet,
altTextAssignments,
} = context
const [headerRow] = sheet.values

Expand All @@ -224,10 +225,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: headerRow.map(columnId => {
if (Object.hasOwn(altTextAssignments, columnId)) {
return { type: "altText" as const, imageColumnId: altTextAssignments[columnId] }
}
const field = fields.find(field => field.id === columnId)
return { type: field?.type ?? "string" }
}),
Comment thread
djohalo2 marked this conversation as resolved.
configureFields: false,
})
Expand Down
114 changes: 114 additions & 0 deletions plugins/google-sheets/src/components/FieldTypeSelectField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import cx from "classnames"
import { framer, type MenuItem } from "framer-plugin"
import React, { useMemo } from "react"
import { isAltTextColumn, type SheetCollectionFieldInput, type VirtualFieldType } from "../sheets"
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

interface Props {
field: SheetCollectionFieldInput
fields: SheetCollectionFieldInput[]
isDisabled: boolean
disabledFieldIds: Set<string>
onFieldTypeChange: (id: string, type: VirtualFieldType, imageColumnId?: string) => void
}

export function FieldTypeSelectField({ field, fields, isDisabled, disabledFieldIds, onFieldTypeChange }: Props) {
const imageColumn = fields.find(candidate => candidate.id === field.imageColumnId)

const fieldTypeLabel = useMemo(() => {
if (isAltTextColumn(field)) {
return imageColumn ? `Alt Text → ${imageColumn.name}` : "Alt Text"
}

return fieldTypeOptions.find(option => option.type === field.type)?.label ?? field.type
}, [field, imageColumn])

const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
const assignedImageColumnIds = new Set(fields.filter(isAltTextColumn).map(candidate => candidate.imageColumnId))

const imageFields = fields.filter(
candidate => candidate.type === "image" && candidate.id !== field.id && !disabledFieldIds.has(candidate.id)
)
const canAssignImageField = (candidate: SheetCollectionFieldInput) =>
!assignedImageColumnIds.has(candidate.id) || candidate.id === field.imageColumnId

const items: MenuItem[] = [
...fieldTypeOptions.map(({ type, label }) => ({
label,
checked: field.type === type,
onAction: () => {
onFieldTypeChange(field.id, type)
},
})),
{
label: "Alt Text",
checked: isAltTextColumn(field),
enabled: imageFields.some(canAssignImageField),
submenu: imageFields.map(candidate => ({
label: candidate.name,
checked: isAltTextColumn(field) && field.imageColumnId === candidate.id,
enabled: canAssignImageField(candidate),
onAction: () => {
onFieldTypeChange(field.id, "altText", candidate.id)
},
})),
},
]

const { left, bottom, width } = event.currentTarget.getBoundingClientRect()
void framer.showContextMenu(items, {
location: {
x: left + width - contextMenuOffset,
y: bottom + contextMenuOffset,
},
placement: "bottom-left",
width,
})
}

return (
<button
type="button"
className={cx("flex w-full min-w-0 items-center gap-1 text-left", isDisabled && "opacity-50")}
disabled={isDisabled}
title={fieldTypeLabel}
onClick={handleMenuOpen}
>
{isAltTextColumn(field) ? (
<span className="flex min-w-0 flex-1 items-center gap-1">
<span className="flex shrink-0 items-center gap-1">
Alt Text
<IconChevron />
</span>
<span className="truncate">{imageColumn?.name ?? "Image"}</span>
</span>
) : (
<span className="min-w-0 flex-1 truncate">{fieldTypeLabel}</span>
)}
<span className="shrink-0 text-content">
<IconChevronDown />
</span>
</button>
)
}
13 changes: 13 additions & 0 deletions plugins/google-sheets/src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,16 @@ export const IconChevron = () => (
></path>
</svg>
)

export const IconChevronDown = () => (
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="8" height="8">
<path
fill="transparent"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
d="m1 2.75 2.293 2.293a1 1 0 0 0 1.414 0L7 2.75"
/>
</svg>
)
100 changes: 49 additions & 51 deletions plugins/google-sheets/src/pages/MapSheetFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,21 @@ import cx from "classnames"
import { framer, useIsAllowedTo } from "framer-plugin"
import { Fragment, useMemo, useState } from "react"
import { CheckboxTextfield } from "../components/CheckboxTextField"
import { FieldTypeSelectField } from "../components/FieldTypeSelectField"
import { IconChevron } from "../components/Icons"
import type {
CellValue,
HeaderRow,
PluginContext,
Row,
SheetCollectionFieldInput,
SheetColumnConfig,
SyncMutationOptions,
VirtualFieldType,
} from "../sheets"
import { isAltTextColumn } 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
Expand Down Expand Up @@ -103,15 +87,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" && Object.hasOwn(context.altTextAssignments, columnId)) {
return { type: "altText", imageColumnId: 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 = (
Expand All @@ -125,10 +113,13 @@ const createFieldConfig = (
const sanitizedName = uniqueColumnNames[columnIndex]
if (!sanitizedName) return null

const { type, imageColumnId } = getColumnConfig(context, sanitizedName, row?.[columnIndex])

return {
id: sanitizedName,
name: sanitizedName,
type: getFieldType(context, sanitizedName, row?.[columnIndex]),
type,
imageColumnId,
} as SheetCollectionFieldInput
})
.filter(isDefined)
Expand Down Expand Up @@ -200,15 +191,25 @@ export function MapSheetFieldsPage({
}))
}

const handleFieldTypeChange = (id: string, type: VirtualFieldType) => {
const handleFieldTypeChange = (id: string, type: VirtualFieldType, imageColumnId?: string) => {
setFieldConfig(current =>
current.map(field => {
if (field.id === id) {
return {
...field,
type,
} as SheetCollectionFieldInput
imageColumnId: type === "altText" ? imageColumnId : undefined,
}
}

if (type !== "image" && isAltTextColumn(field) && field.imageColumnId === id) {
return {
...field,
type: "string",
imageColumnId: undefined,
}
}

return field
})
)
Expand All @@ -220,7 +221,8 @@ export function MapSheetFieldsPage({
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) && !isAltTextColumn(field))
.map(field => {
const maybeOverride = fieldNameOverrides[field.id]
Comment thread
djohalo2 marked this conversation as resolved.
if (maybeOverride) {
Expand All @@ -236,7 +238,7 @@ export function MapSheetFieldsPage({
fields: allFields,
spreadsheetId,
sheetTitle,
colFieldTypes: fieldConfig.map(field => field.type),
columnConfigs: fieldConfig.map(field => ({ type: field.type, imageColumnId: field.imageColumnId })),
ignoredColumns: Array.from(disabledColumns),
slugColumn,
lastSyncedTime: getLastSyncedTime(pluginContext, slugColumn),
Expand Down Expand Up @@ -290,31 +292,27 @@ export function MapSheetFieldsPage({
<div className="flex items-center justify-center">
<IconChevron />
</div>
<select
className={cx("w-full", (isDisabled || !isAllowedToManage) && "opacity-50")}
disabled={isDisabled || !isAllowedToManage}
value={field.type}
onChange={e => {
handleFieldTypeChange(field.id, e.target.value as VirtualFieldType)
}}
title={isAllowedToManage ? undefined : "Insufficient permissions"}
>
{fieldTypeOptions.map(({ type, label }) => (
<option key={type} value={type}>
{label}
</option>
))}
</select>
<input
type="text"
className={cx("w-full", (isDisabled || !isAllowedToManage) && "opacity-50")}
disabled={isDisabled || !isAllowedToManage}
placeholder={field.name}
value={fieldNameOverrides[field.id] ?? ""}
onChange={e => {
handleFieldNameChange(field.id, e.target.value)
}}
<FieldTypeSelectField
field={field}
fields={fieldConfig}
isDisabled={isDisabled || !isAllowedToManage}
disabledFieldIds={disabledColumns}
onFieldTypeChange={handleFieldTypeChange}
/>
{!isAltTextColumn(field) ? (
<input
type="text"
className={cx("w-full", (isDisabled || !isAllowedToManage) && "opacity-50")}
disabled={isDisabled || !isAllowedToManage}
placeholder={field.name}
value={fieldNameOverrides[field.id] ?? ""}
onChange={e => {
handleFieldNameChange(field.id, e.target.value)
}}
/>
) : (
<div />
)}
</Fragment>
)
})}
Expand Down
Loading
Loading