Data connector intent

Data connector integration for external data sources.

Running this example

To run this example locally:

  1. If you haven't already, create a new app in the Developer Portal(opens in a new tab or window). For more information, refer to our Quickstart guide.

  2. In your app's configuration on the Developer Portal(opens in a new tab or window), ensure the "Development URL" is set to http://localhost:8080.

  3. Clone the starter kit:

    git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.git
    cd canva-apps-sdk-starter-kit
    SHELL
  4. Install dependencies:

    npm install
    SHELL
  5. Run the example:

    npm run start data_connector_intent
    SHELL
  6. Click the Preview URL link shown in the terminal to open the example in the Canva editor.

Example app source code

import type {
DataTable,
DataTableImageUpload,
DataTableVideoUpload,
GetDataTableRequest,
} from "@canva/intents/data";
export const saleStageOptions: string[] = [
"Initial Release Stage",
"Construction Stage",
"Final Release Stage",
];
// define the data source structure - the configurable parameters of the data query
export type RealEstateDataConfig = {
selectedStageFilter?: string[];
};
// for a given data source query, get the data from the mock API and transform it to DataTable format
export const getRealEstateData = async (
request: GetDataTableRequest,
): Promise<DataTable> => {
return new Promise((resolve) => {
const { dataSourceRef, limit } = request;
if (dataSourceRef != null) {
const dataRef = JSON.parse(dataSourceRef?.source) as RealEstateDataConfig;
const selectedStages = dataRef.selectedStageFilter?.length
? dataRef.selectedStageFilter
: saleStageOptions;
// get the projects data from the mock API
const projects = getProjects();
// filter projects based on selected stages and transform to DataTable
const dataTable = transformToDataTable(projects, selectedStages);
// ensure we don't exceed row limit
dataTable.rows = dataTable.rows.slice(0, limit.row);
resolve(dataTable);
}
});
};
// mock api response structure
interface RealEstateProject {
name: string;
initialReleaseStage: number;
constructionStage: number;
finalReleaseStage: number;
media: (DataTableImageUpload | DataTableVideoUpload)[];
}
/**
* Sample data for real estate projects.
* Each project has a name, sales stage values, and media assets.
*/
const getProjects = (): RealEstateProject[] => [
{
name: "The Kensington",
initialReleaseStage: getRandomSalesValue(),
constructionStage: getRandomSalesValue(),
finalReleaseStage: getRandomSalesValue(),
media: staticMediaData,
},
{
name: "Horizon Hurstville",
initialReleaseStage: getRandomSalesValue(),
constructionStage: getRandomSalesValue(),
finalReleaseStage: getRandomSalesValue(),
media: staticMediaData,
},
{
name: "Sterling Lane Cove",
initialReleaseStage: getRandomSalesValue(),
constructionStage: getRandomSalesValue(),
finalReleaseStage: getRandomSalesValue(),
media: staticMediaData,
},
{
name: "Surry Hills Village",
initialReleaseStage: getRandomSalesValue(),
constructionStage: getRandomSalesValue(),
finalReleaseStage: getRandomSalesValue(),
media: staticMediaData,
},
{
name: "Willoughby Grounds",
initialReleaseStage: getRandomSalesValue(),
constructionStage: getRandomSalesValue(),
finalReleaseStage: getRandomSalesValue(),
media: staticMediaData,
},
{
name: "Marque Rockdale",
initialReleaseStage: getRandomSalesValue(),
constructionStage: getRandomSalesValue(),
finalReleaseStage: getRandomSalesValue(),
media: staticMediaData,
},
{
name: "Atrium The Retreat",
initialReleaseStage: getRandomSalesValue(),
constructionStage: getRandomSalesValue(),
finalReleaseStage: getRandomSalesValue(),
media: staticMediaData,
},
{
name: "Aura by Aqualand",
initialReleaseStage: getRandomSalesValue(),
constructionStage: getRandomSalesValue(),
finalReleaseStage: getRandomSalesValue(),
media: staticMediaData,
},
];
// Transform mock api data to DataTable based on selected stages
const transformToDataTable = (
projects: RealEstateProject[],
selectedStages: string[],
): DataTable => {
const columnConfigs = [
{ name: "Project", type: "string" as const },
...(selectedStages.includes("Initial Release Stage")
? [{ name: "Initial Release Stage", type: "number" as const }]
: []),
...(selectedStages.includes("Construction Stage")
? [{ name: "Construction Stage", type: "number" as const }]
: []),
...(selectedStages.includes("Final Release Stage")
? [{ name: "Final Release Stage", type: "number" as const }]
: []),
{ name: "Media", type: "media" as const },
];
const rows = projects.map((project) => ({
cells: [
{ type: "string" as const, value: project.name },
...(selectedStages.includes("Initial Release Stage")
? [{ type: "number" as const, value: project.initialReleaseStage }]
: []),
...(selectedStages.includes("Construction Stage")
? [{ type: "number" as const, value: project.constructionStage }]
: []),
...(selectedStages.includes("Final Release Stage")
? [{ type: "number" as const, value: project.finalReleaseStage }]
: []),
{ type: "media" as const, value: project.media },
],
}));
return { columnConfigs, rows };
};
// static media data for the projects
const staticMediaData: (DataTableImageUpload | DataTableVideoUpload)[] = [
{
type: "video_upload",
mimeType: "video/mp4",
url: "https://www.canva.dev/example-assets/video-import/video.mp4",
thumbnailImageUrl:
"https://www.canva.dev/example-assets/video-import/thumbnail-image.jpg",
thumbnailVideoUrl:
"https://www.canva.dev/example-assets/video-import/thumbnail-video.mp4",
width: 405,
height: 720,
aiDisclosure: "none",
},
{
type: "image_upload",
mimeType: "image/jpeg",
url: "https://www.canva.dev/example-assets/image-import/image.jpg",
thumbnailUrl:
"https://www.canva.dev/example-assets/image-import/thumbnail.jpg",
width: 540,
height: 720,
aiDisclosure: "none",
},
];
// Helper function to generate random numbers between 10 and 100
function getRandomSalesValue(): number {
const min = 10;
const max = 100;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
TYPESCRIPT
import type {
RenderSelectionUiRequest,
GetDataTableRequest,
GetDataTableResponse,
} from "@canva/intents/data";
import { prepareDataConnector } from "@canva/intents/data";
import { Alert, AppUiProvider } from "@canva/app-ui-kit";
import { createRoot } from "react-dom/client";
import { SelectionUI } from "./selection_ui";
import "@canva/app-ui-kit/styles.css";
import { getRealEstateData } from "./data";
const root = createRoot(document.getElementById("root") as Element);
prepareDataConnector({
/**
* Gets structured data from an external source.
*
* This action is called in two scenarios:
*
* - During data selection to preview data before import (when {@link RenderSelectionUiRequest.updateDataRef} is called).
* - When refreshing previously imported data (when the user requests an update).
*
* @param request - Parameters for the data fetching operation.
* @returns A promise resolving to either a successful result with data or an error.
*/
getDataTable: async (
request: GetDataTableRequest,
): Promise<GetDataTableResponse> => {
try {
const dataTable = await getRealEstateData(request);
return {
status: "completed",
dataTable,
metadata: {
description:
"Sydney construction project sales in each release stage",
providerInfo: { name: "Demo Sales API" },
},
};
} catch {
return {
status: "app_error",
message: "Failed to process data request",
};
}
},
/**
* Renders a UI component for selecting and configuring data from external sources.
* This UI should allow users to browse data sources, apply filters, and select data.
* When selection is complete, the implementation must call the `updateDataRef`
* callback provided in the params to preview and confirm the data selection.
*
* @param request - parameters that provide context and configuration for the data selection UI.
* Contains invocation context, size limits, and the updateDataRef callback
*/
renderSelectionUi: async (request: RenderSelectionUiRequest) => {
root.render(
<AppUiProvider>
<SelectionUI {...request} />
</AppUiProvider>,
);
},
});
// TODO: Fallback message if you have not turned on the data connector intent.
// You can remove this once your app is correctly configured.
root.render(
<AppUiProvider>
<Alert tone="critical">
If you're seeing this, you need to turn on the data connector intent in
the developer portal for this app.
</Alert>
</AppUiProvider>,
);
TYPESCRIPT
import type { RenderSelectionUiRequest } from "@canva/intents/data";
import {
Alert,
Button,
Rows,
Text,
CheckboxGroup,
FormField,
Title,
} from "@canva/app-ui-kit";
import { useEffect, useState } from "react";
import * as styles from "styles/components.css";
import { saleStageOptions, type RealEstateDataConfig } from "./data";
export const SelectionUI = (request: RenderSelectionUiRequest) => {
const [selectedStageFilter, setSelectedStageFilter] = useState<string[]>();
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
// handle the invocation context to respond to how the app was loaded
// there may be an existing data source to load or an error to display
useEffect(() => {
const { reason } = request.invocationContext;
switch (reason) {
case "data_selection":
// If there's an existing selection, pre-populate the UI
if (request.invocationContext.dataSourceRef) {
try {
const savedParams = JSON.parse(
request.invocationContext.dataSourceRef.source,
) as RealEstateDataConfig;
setSelectedStageFilter(savedParams.selectedStageFilter || []);
} catch {
setError("Failed to load saved selection");
}
}
break;
case "outdated_source_ref":
// data source reference persisted in Canva is outdated. Prompt users to reselect data.
setError(
"Your previously selected data is no longer available. Please make a new selection.",
);
break;
case "app_error":
setError(
request.invocationContext.message ||
"An error occurred with your data",
);
break;
default:
// this should never happen
break;
}
}, [request.invocationContext]);
// use updateDataRef to set the new query config
// this will trigger the getDataTable callback for this connector
const loadData = async () => {
setLoading(true);
setError(null);
setSuccess(false);
try {
const result = await request.updateDataRef({
source: JSON.stringify({ selectedStageFilter }),
title: "Sydney construction project sales in each release stage",
});
if (result.status === "completed") {
setSuccess(true);
} else {
setError(
result.status === "app_error" && "message" in result
? result.message || "An error occurred"
: `Error: ${result.status}`,
);
}
} catch {
setError("Failed to update data");
} finally {
setLoading(false);
}
};
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Title size="large">Project Sales</Title>
{error && <Alert tone="critical" title={error} />}
{success && (
<Alert tone="positive" title="Data preview loaded successfully!" />
)}
<Text variant="bold">
Sydney construction project sales in each release stage
</Text>
<Rows spacing="1u">
<FormField
label="Release Stage"
control={(props) => (
<CheckboxGroup
{...props}
value={selectedStageFilter}
options={saleStageOptions.map((stage) => {
return {
label: stage,
value: stage,
};
})}
onChange={setSelectedStageFilter}
/>
)}
/>
<Button
variant="primary"
onClick={loadData}
loading={loading}
stretch
>
Load Data
</Button>
</Rows>
</Rows>
</div>
);
};
TYPESCRIPT

API Reference

Need Help?