Export data to Airtable

Custom Component Example (Including configuration UI)

/**
 * Questmate Custom Completion Item - Add Airtable Record
 *
 * Permissions:
 * @UseApp {AIRTABLE}
 *
 * Changelog:
 * v0.1 Initial release
 * v0.2 Added email field mapping, fixed reference mapping
 *
 * eslint-disable
 */

enableDebugMode();

const fieldToItemTransformerMap = [

  {
    outputTypes: ["multipleRecordLinks"],
    inputItemFilter: ["CustomV2"],
    transformer: ({ itemType, fieldType, stringValue, rawValue }) => {
      return [Object.values(rawValue)[0]];
    },
  },
  {
    outputTypes: ["singleSelect"],
    inputItemFilter: ["SingleSelect"],
    transformer: ({ itemType, fieldType, stringValue, rawValue }) => {
      return rawValue.items
        .filter((entry) => entry.checked)
        .map((entry) => entry.text)[0];
    },
  },
  {
    outputTypes: ["singleLineText", "multilineText", "email"],
    transformer: ({ stringValue }) => {
      return stringValue;
    },
  },
];

return defineCustomItem((Questmate) => {
  async function getSelectedTable(selectedBaseId, selectedTableId) {
    const tables = await tablesDataSource.retrieve(selectedBaseId);
    return tables.find(
      (table) => !!selectedTableId && table.id === selectedTableId
    );
  }

  Questmate.registerItemRunHandler(async ({ useConfigData, useQuest }) => {
    const [selectedBaseId] = useConfigData("baseId");
    const [selectedTableId] = useConfigData("tableId");
    const [fieldToItemMap] = useConfigData("fieldToItemMap");

    const quest = await useQuest();
    const questRun = await quest.getRun();

    const newRecordData = {};

    const table = await getSelectedTable(selectedBaseId, selectedTableId);

    for (const fieldId of Object.keys(fieldToItemMap)) {
      const item = questRun.getItem(fieldToItemMap[fieldId]);
      const airtableFieldType = table.fields.find(
        (field) => field.id === fieldId
      ).type;

      const transformerConfig = fieldToItemTransformerMap.find(
        (transformerConfig) =>
          transformerConfig.outputTypes.includes(airtableFieldType)
      );

      if (!transformerConfig) {
        continue;
      }

      newRecordData[fieldId] = transformerConfig.transformer({
        stringValue: await item.toString() || "",
        rawValue: item.data || {},
        itemType: item.type,
        fieldType: airtableFieldType
      });
    };

    console.log("NEW RECORD DATA", JSON.stringify(newRecordData));
    
    const insertRecordRequest = await fetch(
      `https://api.airtable.com/v0/${selectedBaseId}/${selectedTableId}`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          records: [
            {
              fields: newRecordData,
            },
          ],
        }),
      }
    );

    const insertRecordResponseData = await insertRecordRequest.json();
    console.log(JSON.stringify(insertRecordResponseData));
  });

  Questmate.registerView("ITEM_CONFIG_VIEW", async ({ useConfigData }) => {
    const viewComponents = [];
    const [selectedBaseId, setSelectedBaseId] = useConfigData("baseId", null);
    const [selectedTableId, setSelectedTableId] = useConfigData(
      "tableId",
      null
    );

    const [fieldToItemMap, setFieldToItemMap] = useConfigData(
      "fieldToItemMap",
      {}
    );

    viewComponents.push({
      id: "base",
      title: "Airtable Base",
      type: "dropdown",
      optionNoun: "Base",
      optionPluralNoun: "Bases",
      value: selectedBaseId,
      getOptions: async () =>
        (await basesDataSource.retrieve()).map((base) => ({
          label: base.name,
          value: base.id,
        })),
      onSelect: async (newBaseId) => {
        const changed = newBaseId !== selectedBaseId;
        if (changed) {
          const isValidBase =
            newBaseId &&
            (await basesDataSource.retrieve()).some(
              (base) => base.id === newBaseId
            );

          setSelectedBaseId(isValidBase ? newBaseId : null);
          setSelectedTableId(null);
        }
      },
    });

    if (selectedBaseId) {
      viewComponents.push({
        id: "table",
        title: "Table",
        type: "dropdown",
        optionNoun: "Table",
        optionPluralNoun: "Tables",
        value: selectedTableId,
        getOptions: async () =>
          (await tablesDataSource.retrieve(selectedBaseId)).map((table) => ({
            label: table.name,
            value: table.id,
          })),
        onSelect: async (newTableId) => {
          const changed = newTableId !== selectedTableId;
          if (changed) {
            const isValidTable =
              newTableId &&
              (await tablesDataSource.retrieve(selectedBaseId)).some(
                (table) => table.id === newTableId
              );
            setSelectedTableId(isValidTable ? newTableId : null);
          }
        },
      });
    }

    if (selectedTableId !== null) {
      const table = await getSelectedTable(selectedBaseId, selectedTableId);
      const fields = table.fields;

      viewComponents.push({
        id: `TextBlock1`,
        type: "text",
        content: "Choose the item to map to each field below.",
      });
      fields.forEach((field) => {
        viewComponents.push({
          id: field.id,
          title: field.name,
          type: "ItemPicker",
          value: fieldToItemMap[field.id],
          onSelect: (itemId) => {
            if (itemId !== fieldToItemMap[field.id]) {
              setFieldToItemMap({
                ...fieldToItemMap,
                [field.id]: itemId,
              });
            }
          },
        });
      });
    }

    return {
      components: viewComponents,
    };
  });

  const basesDataSource = Questmate.registerDataSource({
    id: "bases",
    initialData: [],
    refreshInterval: 60,
    fetcher: () => async () => {
      const basesData = await fetch("https://api.airtable.com/v0/meta/bases");
      const { bases } = await basesData.json();
      if (!bases) {
        return {
          error: {
            message: "Failed to retrieve bases",
            details: { response: basesData },
          },
        };
      }

      return { data: bases };
    },
  });

  const tablesDataSource = Questmate.registerDataSource({
    id: "tables",
    initialData: [],
    refreshInterval: 60,
    fetcher: () => async (baseId) => {
      const tablesData = await fetch(
        `https://api.airtable.com/v0/meta/bases/${baseId}/tables`
      );
      const { tables } = await tablesData.json();

      if (!tables) {
        return {
          error: {
            message: "Failed to retrieve tables",
            details: { baseId, response: tablesData },
          },
        };
      }

      return { data: tables };
    },
  });
});

Questscript (Deprecated)

/**
 * @UseApp {AIRTABLE}
 */

const asset = quest.getItem("asset"); // Airtable dropdown
const inspectorName = quest.getItem("inspectorName"); // Short Text
const inspectionDate = quest.getItem("inspectionDate"); // Date
const currentKilometers = quest.getItem("currentKilometers"); // Short Text
const brakesAndSteeringCheck = quest.getItem("brakesAndSteeringCheck"); // Single Choice
const photos = quest.getItem("photos"); // File upload

const getSelectionFromItem = (item) => {
  return item.data.items
    .filter((entry) => entry.checked)
    .map((entry) => entry.text)[0];
};

const insertRowResponse = await fetch(
  `https://api.airtable.com/v0/${asset.defaults.base}/___INSERT_TABLE_NAME___`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      records: [
        {
          fields: {
            Assets: [asset.data.row],
            "Inspector Name": inspectorName.data.text,
            "Inspection Date": inspectionDate.data.value,
            "Current Kilometers":
              parseInt(currentKilometers.data.text, 10) || 0,
            "Brakes & Steering Check": getSelectionFromItem(
              brakesAndSteeringCheck
            ), // ["OK", "Faulty", "N/A"]
            "Please insert photos of any reported faults":
              photos.data.mediaItems.map((mediaItem) => ({
                filename: mediaItem.name,
                url: mediaItem.url,
              })),
          },
        },
      ],
    }),
  }
);
console.log(JSON.stringify(insertRowResponse));