/* eslint-disable */
enableDebugMode();
/**
* Questmate Custom Component - Airtable Dropdown (Questscript)
*
* Permissions:
* @UseApp {AIRTABLE}
*
* Changelog:
* v0.1 Initial release
* v0.2 Added support for Airtable views
* v0.3 Adding sorting for dropdown options
* v0.4 Improved sorting for dropdown options (numbers, case insensitive)
* v1.0 Use new Custom Item API
*/
return defineCustomItem((Questmate) => {
Questmate.registerView("ITEM_RUN_VIEW", ({ useRunData, useConfigData }) => {
const [baseId] = useConfigData("baseId");
const [tableId] = useConfigData("tableId");
const [viewId] = useConfigData("viewId");
const [columnId] = useConfigData("columnId");
const [selectedRowId, setSelectedRowId] = useRunData("rowId");
return {
components: [
{
id: "row",
type: "dropdown",
title: "Member",
icon: "person",
disabled: false,
optionNoun: "Member",
optionPluralNoun: "Members",
value: selectedRowId,
getOptions: async () => {
return (
await rowsDataSource.retrieve(baseId, tableId, viewId, columnId)
)
.map((row) => ({
label: row.fields[columnId],
value: row.id,
}))
.sort((a, b) =>
a.label.localeCompare(b.label, undefined, {
numeric: true,
sensitivity: "base",
})
);
},
onSelect: setSelectedRowId,
},
],
};
});
Questmate.registerView("ITEM_CONFIG_VIEW", async ({ useConfigData }) => {
const viewComponents = [];
const [selectedBaseId, setSelectedBaseId] = useConfigData("baseId", null);
const [selectedTableId, setSelectedTableId] = useConfigData(
"tableId",
null
);
const [selectedViewId, setSelectedViewId] = useConfigData("viewId", null);
const [selectedColumnId, setSelectedColumnId] = useConfigData(
"columnId",
null
);
async function getSelectedTable() {
const tables = await tablesDataSource.retrieve(selectedBaseId);
return tables.find(
(table) => !!selectedTableId && table.id === selectedTableId
);
}
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);
setSelectedColumnId(null);
setSelectedViewId(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);
setSelectedViewId(null);
setSelectedColumnId(null);
}
},
});
}
if (selectedTableId) {
viewComponents.push({
id: "view",
title: "View",
type: "dropdown",
optionNoun: "View",
optionPluralNoun: "Views",
value: selectedViewId,
getOptions: async () => {
const selectedTable = await getSelectedTable();
return selectedTable.views.map((view) => ({
label: view.name,
value: view.id,
}));
},
onSelect: async (newViewId) => {
const changed = newViewId !== selectedViewId;
if (changed) {
const isValidView =
newViewId &&
(await getSelectedTable()).views.some(
(view) => view.id === newViewId
);
setSelectedViewId(isValidView ? newViewId : null);
setSelectedColumnId(null);
}
},
});
}
if (selectedViewId) {
viewComponents.push({
id: "column",
title: "Column",
type: "dropdown",
optionNoun: "Column",
optionPluralNoun: "Columns",
value: selectedColumnId,
getOptions: async () => {
const selectedTable = await getSelectedTable();
return selectedTable.fields.map((field) => ({
label: field.name,
value: field.id,
}));
},
onSelect: async (newColumnId) => {
const changed = newColumnId !== selectedColumnId;
if (changed) {
const isValidColumn =
newColumnId &&
(await getSelectedTable()).fields.some(
(field) => field.id === newColumnId
);
setSelectedColumnId(isValidColumn ? newColumnId : null);
}
},
});
}
return {
components: viewComponents,
};
});
const basesDataSource = Questmate.registerDataSource({
id: "bases",
initialData: [],
refreshInterval: 15,
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: 15,
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 };
},
});
const rowsDataSource = Questmate.registerDataSource({
id: "rows",
initialData: [],
refreshInterval: 60,
aggregator: ({ results, staleResults }) => {
// combine stale pages with fresh pages to provide most up-to-date information
return (
[...results, ...staleResults]
.flatMap((result) => result.data)
// filter out duplicate row data
.filter(
(row, index, self) =>
self.findIndex(({ id }) => id === row.id) === index
)
);
},
fetcher:
({ nextPageMarker }) =>
async (baseId, tableId, viewId, columnId) => {
const response = await fetch(
`https://api.airtable.com/v0/${baseId}/${tableId}?maxRecords=2000&returnFieldsByFieldId=true&fields%5B%5D=${columnId}${
nextPageMarker ? `&offset=${nextPageMarker}` : ""
}&view=${viewId}`
);
if (response.status === 429) {
return {
error: {
message: "Rate limit reached. Retrying soon.",
retryAfter: 30,
},
};
}
const body = await response.json();
if (!body.records || body.records.length === 0) {
return {
error: {
message: "Failed to retrieve rows",
details: {
response,
},
},
};
}
return {
nextPageMarker: body.offset,
data: body.records
// filter out rows without labels (and ids, which shouldn't happen)
.filter(
({ id, fields }) => Boolean(id) && Boolean(fields[columnId])
),
};
},
});
});
Copyright © Questmate Pty Ltd.