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

// For usage information, see the README.md file.
import type {
DataTable,
DataTableImageUpload,
DataTableVideoUpload,
GetDataTableRequest,
} from "@canva/intents/data";
// Available filter options for real estate project sales stages
export const saleStageOptions: string[] = [
"Initial Release Stage",
"Construction Stage",
"Final Release Stage",
];
// Configuration object that defines the structure of a data source query
export type RealEstateDataConfig = {
selectedStageFilter?: string[];
};
// Fetches data from the mock API and transforms it into Canva's DataTable format
export const getRealEstateData = async (
request: GetDataTableRequest,
): Promise<DataTable> => {
return new Promise((resolve) => {
const { dataSourceRef, limit } = request;
if (dataSourceRef != null) {
// Parse the saved data source configuration from the request
const dataRef = JSON.parse(dataSourceRef?.source) as RealEstateDataConfig;
// Use selected stages from config, or default to all stages if none selected
const selectedStages = dataRef.selectedStageFilter?.length
? dataRef.selectedStageFilter
: saleStageOptions;
// Fetch mock project data
const projects = getProjects();
// Transform raw project data to Canva's DataTable format
const dataTable = transformToDataTable(projects, selectedStages);
// Apply row limit as specified by Canva's data connector constraints
dataTable.rows = dataTable.rows.slice(0, limit.row);
resolve(dataTable);
}
});
};
// Structure representing a real estate project from the mock API
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,
},
];
// Converts the mock API project data into Canva's DataTable format with dynamic column configuration
const transformToDataTable = (
projects: RealEstateProject[],
selectedStages: string[],
): DataTable => {
// Define column structure based on user's selected stages
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 },
];
// Generate table rows with data cells matching the column structure
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 assets used for demonstration purposes in all 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",
},
];
// Generates random sales values for demonstration purposes
function getRandomSalesValue(): number {
const min = 10;
const max = 100;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
TYPESCRIPT
// For usage information, see the README.md file.
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);
// Configure the data connector intent with required callbacks
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>,
);
},
});
// Fallback message displayed when the data connector intent is not enabled.
// Remove this once your app is correctly configured in the developer portal.
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
// For usage information, see the README.md file.
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 different invocation contexts (data selection, errors, outdated references)
// This determines how to initialize the UI based on why the data connector was opened
useEffect(() => {
const { reason } = request.invocationContext;
switch (reason) {
case "data_selection":
// Pre-populate UI with existing data source configuration if available
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":
// The data source reference stored in Canva is no longer valid
setError(
"Your previously selected data is no longer available. Please make a new selection.",
);
break;
case "app_error":
// Display error message from previous data fetch attempt
setError(
request.invocationContext.message ||
"An error occurred with your data",
);
break;
default:
break;
}
}, [request.invocationContext]);
// Sends the selected data configuration to Canva and triggers data preview
// This calls the getDataTable function with the current selection
const loadData = async () => {
setLoading(true);
setError(null);
setSuccess(false);
try {
// Call Canva's updateDataRef with the current configuration
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
# Data connector intent
This example demonstrates how to build a data connector intent app that allows users to import structured data from external sources into Canva designs. The app shows how to implement both the data fetching logic and the selection UI for filtering and configuring data imports.
For API reference docs and instructions on running this example, see: <https://www.canva.dev/docs/apps/examples/data-connector-intent/>.
NOTE: This example differs from what is expected for public apps to pass a Canva review:
- **Static assets**: This example uses static Canva-hosted URLs for media content. Production apps should use CDN/hosting services and implement the `upload` function from `@canva/asset` package for real media uploads.
- **Localization**: Text content is hardcoded in English. Production apps require proper internationalization using the `@canva/app-i18n-kit` package for multi-language support.
- **Data validation**: The example assumes well-formed data responses. Production apps should implement robust data validation and sanitization.
- **API integration**: This example uses mock data. Production apps need to implement proper API authentication, rate limiting, and error handling for external data sources.
MARKDOWN

API reference

Need help?