Skip to content

Commit

Permalink
add support for all airtable field types (#234)
Browse files Browse the repository at this point in the history
* add 'key' and 'path' properties to table in airtable config

* refactor: AirtableDataSource output query mappings to improve path handling

* feat: add support for all airtable field types

- Automatically assign output mapping types to airtable fields
- Improved field mapping logic to filter unsupported fields and handle selected table fields more effectively.

* refactor: update field type handling to use 'currency' instead of 'price'
  • Loading branch information
shekharnwagh authored Dec 10, 2024
1 parent 4e1031d commit 78e3755
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 35 deletions.
2 changes: 1 addition & 1 deletion docs/extending/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ The `get_input_schema` method defines the input data expected by the query. The
- `image_alt`
- `image_url`
- `number`
- `price`
- `currency`
- `string`

#### Example
Expand Down
19 changes: 15 additions & 4 deletions inc/Config/QueryRunner/QueryRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,10 @@ public function execute( array $input_variables ): array|WP_Error {
* @param string $field_type The field type.
* @return string The field value.
*/
protected function get_field_value( array|string $field_value, string $default_value = '', string $field_type = 'string' ): string {
protected function get_field_value( array|string $field_value, array $mapping ): string {
$default_value = $mapping['default_value'] ?? '';
$field_type = $mapping['type'];

$field_value_single = is_array( $field_value ) && count( $field_value ) > 1 ? $field_value : ( $field_value[0] ?? $default_value );

switch ( $field_type ) {
Expand All @@ -251,10 +254,18 @@ protected function get_field_value( array|string $field_value, string $default_v
case 'html':
return $field_value_single;

case 'price':
return sprintf( '$%s', number_format( (float) $field_value_single, 2 ) );
case 'currency':
$currency_symbol = $mapping['prefix'] ?? '$';
return sprintf( '%s%s', $currency_symbol, number_format( (float) $field_value_single, 2 ) );

case 'string':
if ( is_array( $field_value_single ) ) {
// Ensure all elements are strings and filter out non-string values
$string_values = array_filter( $field_value_single, '\is_string' );
if ( ! empty( $string_values ) ) {
return wp_strip_all_tags( implode( ', ', $string_values ) );
}
}
return wp_strip_all_tags( $field_value_single );
}

Expand Down Expand Up @@ -304,7 +315,7 @@ protected function map_fields( string|array|object|null $response_data, bool $is

// JSONPath always returns values in an array, even if there's only one value.
// Because we're mostly interested in single values for field mapping, unwrap the array if it's only one item.
$field_value_single = self::get_field_value( $field_value, $mapping['default_value'] ?? '', $mapping['type'] );
$field_value_single = self::get_field_value( $field_value, $mapping );
}

return array_merge( $mapping, [
Expand Down
2 changes: 1 addition & 1 deletion inc/Editor/BlockPatterns/BlockPatterns.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public static function register_default_block_pattern( string $block_name, strin
break;

case 'base64':
case 'price':
case 'currency':
$bindings['paragraphs'][] = [
'content' => [ $field, $name ],
];
Expand Down
25 changes: 21 additions & 4 deletions inc/Integrations/Airtable/AirtableDataSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,23 @@ class AirtableDataSource extends HttpDataSource {
'items' => [
'type' => 'object',
'properties' => [
'name' => [ 'type' => 'string' ],
'key' => [ 'type' => 'string' ],
'name' => [
'type' => 'string',
'required' => false,
],
'type' => [
'type' => 'string',
'required' => false,
],
'path' => [
'type' => 'string',
'required' => false,
],
'prefix' => [
'type' => 'string',
'required' => false,
],
],
],
],
Expand Down Expand Up @@ -121,11 +133,16 @@ public function ___temp_get_query(): AirtableGetItemQuery|\WP_Error {
];

foreach ( $this->config['tables'][0]['output_query_mappings'] as $mapping ) {
$output_schema['mappings'][ ucfirst( $mapping['name'] ) ] = [
'name' => $mapping['name'],
'path' => '$.fields.' . $mapping['name'],
$mapping_key = $mapping['key'];
$output_schema['mappings'][ $mapping_key ] = [
'name' => $mapping['name'] ?? $mapping_key,
'path' => $mapping['path'] ?? '$.fields["' . $mapping_key . '"]',
'type' => $mapping['type'] ?? 'string',
];

if ( 'currency' === $mapping['type'] && isset( $mapping['prefix'] ) ) {
$output_schema['mappings'][ $mapping_key ]['prefix'] = $mapping['prefix'];
}
}

return AirtableGetItemQuery::from_array([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function get_output_schema(): array {
'price' => [
'name' => 'Item price',
'path' => '$.data.product.priceRange.maxVariantPrice.amount',
'type' => 'price',
'type' => 'currency',
],
'variant_id' => [
'name' => 'Variant ID',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function get_output_schema(): array {
'price' => [
'name' => 'Item price',
'path' => '$.node.priceRange.maxVariantPrice.amount',
'type' => 'price',
'type' => 'currency',
],
'image_url' => [
'name' => 'Item image URL',
Expand Down
2 changes: 1 addition & 1 deletion src/blocks/remote-data-container/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ export const REMOTE_DATA_REST_API_URL = getRestUrl();
export const CONTAINER_CLASS_NAME = getClassName( 'container' );

export const IMAGE_FIELD_TYPES = [ 'image_alt', 'image_url' ];
export const TEXT_FIELD_TYPES = [ 'number', 'base64', 'price', 'string' ];
export const TEXT_FIELD_TYPES = [ 'number', 'base64', 'currency', 'string' ];
export const BUTTON_FIELD_TYPES = [ 'button_url' ];
49 changes: 31 additions & 18 deletions src/data-sources/airtable/AirtableSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { useEffect, useMemo, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { ChangeEvent } from 'react';

import { SUPPORTED_AIRTABLE_TYPES } from '@/data-sources/airtable/constants';
import { AirtableFormState } from '@/data-sources/airtable/types';
import { getAirtableOutputQueryMappingValue } from '@/data-sources/airtable/utils';
import { DataSourceForm } from '@/data-sources/components/DataSourceForm';
import { DataSourceFormActions } from '@/data-sources/components/DataSourceFormActions';
import PasswordInputControl from '@/data-sources/components/PasswordInputControl';
Expand All @@ -15,7 +17,11 @@ import {
useAirtableApiUserId,
} from '@/data-sources/hooks/useAirtable';
import { useDataSources } from '@/data-sources/hooks/useDataSources';
import { AirtableConfig, SettingsComponentProps } from '@/data-sources/types';
import {
AirtableConfig,
AirtableOutputQueryMappingValue,
SettingsComponentProps,
} from '@/data-sources/types';
import { getConnectionMessage } from '@/data-sources/utils';
import { useForm } from '@/hooks/useForm';
import { useSettingsContext } from '@/settings/hooks/useSettingsNav';
Expand Down Expand Up @@ -51,7 +57,7 @@ const getInitialStateFromConfig = ( config?: AirtableConfig ): AirtableFormState
name: table.name,
};
initialStateFromConfig.table_fields = new Set(
table.output_query_mappings.map( ( { name } ) => name )
table.output_query_mappings.map( ( { key } ) => key )
);
}
}
Expand Down Expand Up @@ -105,6 +111,11 @@ export const AirtableSettings = ( {
return;
}

const selectedTable = tables?.find( table => table.id === state.table?.id );
if ( ! selectedTable ) {
return;
}

const airtableConfig: AirtableConfig = {
uuid: uuidFromProps ?? '',
service: 'airtable',
Expand All @@ -114,10 +125,18 @@ export const AirtableSettings = ( {
{
id: state.table.id,
name: state.table.name,
output_query_mappings: Array.from( state.table_fields ).map( name => ( {
name,
type: name.endsWith( '.url' ) ? 'image_url' : 'string',
} ) ),
output_query_mappings: Array.from( state.table_fields )
.map( key => {
const field = selectedTable.fields.find( tableField => tableField.name === key );
if ( field ) {
return getAirtableOutputQueryMappingValue( field );
}
/**
* Remove any fields which are not from this table or not supported.
*/
return null;
} )
.filter( Boolean ) as AirtableOutputQueryMappingValue[],
},
],
slug: state.slug,
Expand Down Expand Up @@ -148,6 +167,11 @@ export const AirtableSettings = ( {
} else if ( id === 'table' ) {
const selectedTable = tables?.find( table => table.id === value );
newValue = { id: value, name: selectedTable?.name ?? '' };

if ( value !== state.table?.id ) {
// Reset the selected fields when the table changes.
handleOnChange( 'table_fields', new Set< string >() );
}
}
handleOnChange( id, newValue );
}
Expand Down Expand Up @@ -284,19 +308,8 @@ export const AirtableSettings = ( {

if ( selectedTable ) {
selectedTable.fields.forEach( field => {
const simpleFieldTypes = [
'singleLineText',
'multilineText',
'email',
'phoneNumber',
'url',
'number',
];

if ( simpleFieldTypes.includes( field.type ) ) {
if ( SUPPORTED_AIRTABLE_TYPES.includes( field.type ) ) {
newAvailableTableFields.push( field.name );
} else if ( field.type === 'multipleAttachments' ) {
newAvailableTableFields.push( `${ field.name }[0].url` );
}
} );
}
Expand Down
65 changes: 65 additions & 0 deletions src/data-sources/airtable/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
export const AIRTABLE_STRING_TYPES = Object.freeze(
new Set( [
'singleLineText',
'multilineText',
'email',
'phoneNumber',
'richText',
'barcode',
'singleSelect',
'date',
'dateTime',
'lastModifiedTime',
'createdTime',
'multipleRecordLinks',
'rollup',
'externalSyncSource',
] )
);

export const AIRTABLE_NUMBER_TYPES = Object.freeze(
new Set( [ 'number', 'autoNumber', 'rating', 'duration', 'count', 'percent' ] )
);

export const AIRTABLE_USER_TYPES = Object.freeze(
new Set( [ 'createdBy', 'lastModifiedBy', 'singleCollaborator' ] )
);

export const SUPPORTED_AIRTABLE_TYPES = Object.freeze( [
// String types
'singleLineText',
'multilineText',
'email',
'phoneNumber',
'richText',
'barcode',
'singleSelect',
'multipleSelects',
'date',
'dateTime',
'lastModifiedTime',
'createdTime',
'multipleRecordLinks',
'rollup',
'externalSyncSource',
// Number types
'number',
'autoNumber',
'rating',
'duration',
'count',
'percent',
// User types
'createdBy',
'lastModifiedBy',
'singleCollaborator',
// Other types
'multipleCollaborator',
'url',
'button',
'currency',
'checkbox',
'multipleAttachments',
'formula',
'lookup',
] );
26 changes: 24 additions & 2 deletions src/data-sources/airtable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,35 @@ export interface AirtableTable {
syncStatus: 'complete' | 'pending';
}

interface AirtableField {
/**
* Represents an Airtable field configuration.
* @see https://airtable.com/developers/web/api/model/table-model#fields
*/
export interface AirtableField {
id: string;
name: string;
type: string;
description: string | null;
options?: {
[ key: string ]: unknown;
choices?: Array< {
id: string;
name: string;
color?: string;
} >;
precision?: number;
symbol?: string;
format?: string;
foreignTableId?: string;
relationship?: 'many' | 'one';
symmetricColumnId?: string;
result?: {
type: string;
options?: {
precision?: number;
symbol?: string;
format?: string;
};
};
};
}

Expand Down
Loading

0 comments on commit 78e3755

Please sign in to comment.