@canva/cli 0.0.1-beta.30 → 0.0.1-beta.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +313 -276
- package/package.json +1 -1
- package/templates/data_connector/package.json +1 -1
- package/templates/data_connector/src/api/data_sources/designs.tsx +60 -17
- package/templates/data_connector/src/api/data_sources/templates.tsx +66 -24
- package/templates/data_connector/src/api/fetch_data_table.ts +12 -8
- package/templates/data_connector/src/app.tsx +3 -7
- package/templates/data_connector/src/components/inputs/messages.tsx +19 -0
- package/templates/data_connector/src/components/inputs/search_filter.tsx +108 -0
- package/templates/data_connector/src/context/app_context.tsx +10 -10
- package/templates/data_connector/src/entrypoint.tsx +5 -8
- package/templates/data_connector/src/index.tsx +10 -10
- package/templates/data_connector/src/utils/data_params.ts +9 -9
- package/templates/data_connector/src/utils/data_table.ts +17 -2
- package/templates/data_connector/src/utils/fetch_result.ts +6 -6
- package/templates/data_connector/src/utils/tests/data_table.test.ts +6 -6
package/package.json
CHANGED
|
@@ -16,9 +16,11 @@ import { SelectField } from "src/components/inputs/select_field";
|
|
|
16
16
|
import { dateCell, numberCell, stringCell } from "src/utils";
|
|
17
17
|
import { Paths } from "src/routes";
|
|
18
18
|
import { useNavigate } from "react-router-dom";
|
|
19
|
-
import { useState } from "react";
|
|
19
|
+
import { useEffect, useState } from "react";
|
|
20
|
+
import { SearchFilter } from "src/components/inputs/search_filter";
|
|
20
21
|
|
|
21
22
|
export interface DesignsDataSource extends DataSourceConfig {
|
|
23
|
+
query: string;
|
|
22
24
|
ownership: "any" | "owned" | "shared";
|
|
23
25
|
sort_by:
|
|
24
26
|
| "relevance"
|
|
@@ -41,6 +43,7 @@ export const designsSource = new DataSourceHandler<
|
|
|
41
43
|
>(
|
|
42
44
|
{
|
|
43
45
|
schema: "designs/v1",
|
|
46
|
+
query: "",
|
|
44
47
|
ownership: "any",
|
|
45
48
|
sort_by: "relevance",
|
|
46
49
|
},
|
|
@@ -77,7 +80,14 @@ export const designsSource = new DataSourceHandler<
|
|
|
77
80
|
rowLimit: number,
|
|
78
81
|
signal: AbortSignal | undefined,
|
|
79
82
|
) =>
|
|
80
|
-
getDesigns(
|
|
83
|
+
getDesigns(
|
|
84
|
+
authToken,
|
|
85
|
+
rowLimit,
|
|
86
|
+
signal,
|
|
87
|
+
source.query,
|
|
88
|
+
source.ownership,
|
|
89
|
+
source.sort_by,
|
|
90
|
+
),
|
|
81
91
|
DesignSelection,
|
|
82
92
|
DesignsSourceConfig,
|
|
83
93
|
);
|
|
@@ -86,15 +96,24 @@ export async function getDesigns(
|
|
|
86
96
|
authToken: string,
|
|
87
97
|
rowLimit: number,
|
|
88
98
|
signal: AbortSignal | undefined,
|
|
99
|
+
query: string,
|
|
89
100
|
ownership: string,
|
|
90
101
|
sort_by: string,
|
|
91
102
|
continuation?: string,
|
|
92
103
|
allItems: CanvaDesign[] = [],
|
|
93
104
|
): Promise<CanvaDesign[]> {
|
|
94
105
|
const baseUrl = `https://api.canva.com/rest/v1/designs`;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
const params = new URLSearchParams();
|
|
107
|
+
if (continuation) {
|
|
108
|
+
params.set("continuation", continuation);
|
|
109
|
+
} else {
|
|
110
|
+
if (query) {
|
|
111
|
+
params.set("query", query);
|
|
112
|
+
}
|
|
113
|
+
params.set("ownership", ownership);
|
|
114
|
+
params.set("sort_by", sort_by);
|
|
115
|
+
}
|
|
116
|
+
const url = `${baseUrl}?${params.toString()}`;
|
|
98
117
|
|
|
99
118
|
return fetch(url, {
|
|
100
119
|
headers: {
|
|
@@ -118,6 +137,7 @@ export async function getDesigns(
|
|
|
118
137
|
authToken,
|
|
119
138
|
rowLimit,
|
|
120
139
|
signal,
|
|
140
|
+
query,
|
|
121
141
|
ownership,
|
|
122
142
|
sort_by,
|
|
123
143
|
data.continuation,
|
|
@@ -171,14 +191,29 @@ function DesignSelection() {
|
|
|
171
191
|
function DesignsSourceConfig(sourceConfig: DesignsDataSource) {
|
|
172
192
|
const intl = useIntl();
|
|
173
193
|
const { loadDataSource } = useAppContext();
|
|
194
|
+
const [query, setQuery] = useState<string>(sourceConfig.query);
|
|
174
195
|
const [ownership, setOwnership] = useState<string>(sourceConfig.ownership);
|
|
175
196
|
const [sortOrder, setSortOrder] = useState<string>(sourceConfig.sort_by);
|
|
176
197
|
const [isLoading, setIsLoading] = useState(false);
|
|
177
198
|
|
|
199
|
+
const [filterCount, setFilterCount] = useState(0);
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
// Update the filter count based on the selected filters
|
|
202
|
+
// consider a filter to be applied if not set to the default value
|
|
203
|
+
setFilterCount(
|
|
204
|
+
(ownership !== "any" ? 1 : 0) + (sortOrder !== "relevance" ? 1 : 0),
|
|
205
|
+
);
|
|
206
|
+
}, [ownership, sortOrder]);
|
|
207
|
+
const resetFilters = () => {
|
|
208
|
+
setOwnership("any");
|
|
209
|
+
setSortOrder("relevance");
|
|
210
|
+
};
|
|
211
|
+
|
|
178
212
|
const loadDesigns = async () => {
|
|
179
213
|
setIsLoading(true);
|
|
180
214
|
loadDataSource("Canva Designs", {
|
|
181
215
|
schema: "designs/v1",
|
|
216
|
+
query,
|
|
182
217
|
ownership,
|
|
183
218
|
sort_by: sortOrder,
|
|
184
219
|
} as DesignsDataSource).then(() => {
|
|
@@ -223,18 +258,26 @@ function DesignsSourceConfig(sourceConfig: DesignsDataSource) {
|
|
|
223
258
|
showBack={true}
|
|
224
259
|
/>
|
|
225
260
|
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
261
|
+
<SearchFilter
|
|
262
|
+
value={query}
|
|
263
|
+
onChange={setQuery}
|
|
264
|
+
filterCount={filterCount}
|
|
265
|
+
resetFilters={resetFilters}
|
|
266
|
+
>
|
|
267
|
+
<SelectField
|
|
268
|
+
label={intl.formatMessage(ownershipFilter.label)}
|
|
269
|
+
options={ownershipOptions}
|
|
270
|
+
value={ownership}
|
|
271
|
+
onChange={setOwnership}
|
|
272
|
+
/>
|
|
273
|
+
<SelectField
|
|
274
|
+
label={intl.formatMessage(sortOrderField.label)}
|
|
275
|
+
options={sortOrderOptions}
|
|
276
|
+
value={sortOrder}
|
|
277
|
+
onChange={setSortOrder}
|
|
278
|
+
/>
|
|
279
|
+
</SearchFilter>
|
|
280
|
+
|
|
238
281
|
<Button
|
|
239
282
|
variant="primary"
|
|
240
283
|
loading={isLoading}
|
|
@@ -21,9 +21,11 @@ import { SelectField } from "src/components/inputs/select_field";
|
|
|
21
21
|
import { Header } from "src/components";
|
|
22
22
|
import { useNavigate } from "react-router-dom";
|
|
23
23
|
import { Paths } from "src/routes";
|
|
24
|
-
import { useState } from "react";
|
|
24
|
+
import { useEffect, useState } from "react";
|
|
25
|
+
import { SearchFilter } from "src/components/inputs/search_filter";
|
|
25
26
|
|
|
26
27
|
export interface BrandTemplatesDataSource extends DataSourceConfig {
|
|
28
|
+
query: string;
|
|
27
29
|
dataset: "any" | "non_empty" | "empty";
|
|
28
30
|
ownership: "any" | "owned" | "shared";
|
|
29
31
|
sort_by:
|
|
@@ -48,6 +50,7 @@ export const brandTemplatesSource = new DataSourceHandler<
|
|
|
48
50
|
>(
|
|
49
51
|
{
|
|
50
52
|
schema: "brand_templates/v1",
|
|
53
|
+
query: "",
|
|
51
54
|
dataset: "any",
|
|
52
55
|
ownership: "any",
|
|
53
56
|
sort_by: "relevance",
|
|
@@ -94,8 +97,9 @@ export const brandTemplatesSource = new DataSourceHandler<
|
|
|
94
97
|
authToken,
|
|
95
98
|
rowLimit,
|
|
96
99
|
signal,
|
|
97
|
-
source.
|
|
100
|
+
source.query,
|
|
98
101
|
source.ownership,
|
|
102
|
+
source.dataset,
|
|
99
103
|
source.sort_by,
|
|
100
104
|
),
|
|
101
105
|
BrandTemplatesSelection,
|
|
@@ -106,6 +110,7 @@ export async function getBrandTemplates(
|
|
|
106
110
|
authToken: string,
|
|
107
111
|
rowLimit: number,
|
|
108
112
|
signal: AbortSignal | undefined,
|
|
113
|
+
query: string,
|
|
109
114
|
ownership: string,
|
|
110
115
|
dataset: string,
|
|
111
116
|
sort_by: string,
|
|
@@ -113,9 +118,19 @@ export async function getBrandTemplates(
|
|
|
113
118
|
allItems: CanvaBrandTemplate[] = [],
|
|
114
119
|
): Promise<CanvaBrandTemplate[]> {
|
|
115
120
|
const baseUrl = `https://api.canva.com/rest/v1/brand-templates`;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
|
|
122
|
+
const params = new URLSearchParams();
|
|
123
|
+
if (continuation) {
|
|
124
|
+
params.set("continuation", continuation);
|
|
125
|
+
} else {
|
|
126
|
+
if (query) {
|
|
127
|
+
params.set("query", query);
|
|
128
|
+
}
|
|
129
|
+
params.set("ownership", ownership);
|
|
130
|
+
params.set("dataset", dataset);
|
|
131
|
+
params.set("sort_by", sort_by);
|
|
132
|
+
}
|
|
133
|
+
const url = `${baseUrl}?${params.toString()}`;
|
|
119
134
|
|
|
120
135
|
return fetch(url, {
|
|
121
136
|
headers: {
|
|
@@ -139,6 +154,7 @@ export async function getBrandTemplates(
|
|
|
139
154
|
authToken,
|
|
140
155
|
rowLimit,
|
|
141
156
|
signal,
|
|
157
|
+
query,
|
|
142
158
|
ownership,
|
|
143
159
|
dataset,
|
|
144
160
|
sort_by,
|
|
@@ -191,16 +207,34 @@ function BrandTemplatesSelection() {
|
|
|
191
207
|
}
|
|
192
208
|
|
|
193
209
|
function BrandTemplatesSourceConfig(sourceConfig: BrandTemplatesDataSource) {
|
|
210
|
+
const intl = useIntl();
|
|
194
211
|
const { loadDataSource } = useAppContext();
|
|
212
|
+
const [query, setQuery] = useState<string>(sourceConfig.query);
|
|
195
213
|
const [ownership, setOwnership] = useState<string>(sourceConfig.ownership);
|
|
196
214
|
const [sortOrder, setSortOrder] = useState<string>(sourceConfig.sort_by);
|
|
197
215
|
const [dataset, setDataset] = useState<string>(sourceConfig.dataset);
|
|
198
216
|
const [isLoading, setIsLoading] = useState(false);
|
|
199
|
-
|
|
217
|
+
|
|
218
|
+
const [filterCount, setFilterCount] = useState(0);
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
// Update the filter count based on the selected filters
|
|
221
|
+
// consider a filter to be applied if not set to the default value
|
|
222
|
+
setFilterCount(
|
|
223
|
+
(ownership !== "any" ? 1 : 0) +
|
|
224
|
+
(dataset !== "any" ? 1 : 0) +
|
|
225
|
+
(sortOrder !== "relevance" ? 1 : 0),
|
|
226
|
+
);
|
|
227
|
+
}, [ownership, dataset, sortOrder]);
|
|
228
|
+
const resetFilters = () => {
|
|
229
|
+
setOwnership("any");
|
|
230
|
+
setDataset("any");
|
|
231
|
+
setSortOrder("relevance");
|
|
232
|
+
};
|
|
200
233
|
|
|
201
234
|
const loadTemplates = async () => {
|
|
202
235
|
loadDataSource("Canva Brand Templates", {
|
|
203
236
|
schema: "brand_templates/v1",
|
|
237
|
+
query,
|
|
204
238
|
ownership,
|
|
205
239
|
dataset,
|
|
206
240
|
sort_by: sortOrder,
|
|
@@ -251,24 +285,32 @@ function BrandTemplatesSourceConfig(sourceConfig: BrandTemplatesDataSource) {
|
|
|
251
285
|
showBack={true}
|
|
252
286
|
/>
|
|
253
287
|
<Rows spacing="2u">
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
288
|
+
<SearchFilter
|
|
289
|
+
value={query}
|
|
290
|
+
onChange={setQuery}
|
|
291
|
+
filterCount={filterCount}
|
|
292
|
+
resetFilters={resetFilters}
|
|
293
|
+
>
|
|
294
|
+
<SelectField
|
|
295
|
+
label={intl.formatMessage(ownershipFilter.label)}
|
|
296
|
+
options={ownershipOptions}
|
|
297
|
+
value={ownership}
|
|
298
|
+
onChange={setOwnership}
|
|
299
|
+
/>
|
|
300
|
+
<SelectField
|
|
301
|
+
label={intl.formatMessage(datasetFilter.label)}
|
|
302
|
+
options={datasetOptions}
|
|
303
|
+
value={dataset}
|
|
304
|
+
onChange={setDataset}
|
|
305
|
+
/>
|
|
306
|
+
<SelectField
|
|
307
|
+
label={intl.formatMessage(sortOrderField.label)}
|
|
308
|
+
options={sortOrderOptions}
|
|
309
|
+
value={sortOrder}
|
|
310
|
+
onChange={setSortOrder}
|
|
311
|
+
/>
|
|
312
|
+
</SearchFilter>
|
|
313
|
+
|
|
272
314
|
<Button
|
|
273
315
|
variant="primary"
|
|
274
316
|
loading={isLoading}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
GetDataTableRequest,
|
|
3
|
+
GetDataTableResponse,
|
|
4
4
|
} from "@canva/intents/data";
|
|
5
5
|
import { DATA_SOURCES } from "./data_sources";
|
|
6
6
|
import {
|
|
@@ -13,16 +13,16 @@ import { DataAPIError } from ".";
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* This function handles parsing the data fetch parameters and calling the appropriate handler for the data source.
|
|
16
|
-
* @param
|
|
16
|
+
* @param request
|
|
17
17
|
* @param authToken
|
|
18
18
|
* @returns
|
|
19
19
|
*/
|
|
20
20
|
export const buildDataTableResult = async (
|
|
21
|
-
|
|
21
|
+
request: GetDataTableRequest,
|
|
22
22
|
authToken?: string,
|
|
23
|
-
): Promise<
|
|
24
|
-
const source = JSON.parse(
|
|
25
|
-
const rowLimit =
|
|
23
|
+
): Promise<GetDataTableResponse> => {
|
|
24
|
+
const source = JSON.parse(request.dataSourceRef.source);
|
|
25
|
+
const rowLimit = request.limit.row - 1; // -1 for the header row
|
|
26
26
|
|
|
27
27
|
const dataHandler = DATA_SOURCES.find((handler) =>
|
|
28
28
|
handler.matchSource(source),
|
|
@@ -37,8 +37,12 @@ export const buildDataTableResult = async (
|
|
|
37
37
|
source,
|
|
38
38
|
authToken || "",
|
|
39
39
|
rowLimit,
|
|
40
|
-
|
|
40
|
+
request.signal,
|
|
41
41
|
);
|
|
42
|
+
if (dataTable.rows.length === 0) {
|
|
43
|
+
// if the data table is empty, return an error to prompt the user to reconfigure the data source
|
|
44
|
+
return appError("No results found.");
|
|
45
|
+
}
|
|
42
46
|
return completeDataTable(dataTable);
|
|
43
47
|
} catch (error) {
|
|
44
48
|
if (error instanceof DataAPIError) {
|
|
@@ -5,17 +5,13 @@ import { AppUiProvider } from "@canva/app-ui-kit";
|
|
|
5
5
|
import { AppI18nProvider } from "@canva/app-i18n-kit";
|
|
6
6
|
import { ErrorBoundary } from "react-error-boundary";
|
|
7
7
|
import { ErrorPage } from "./pages";
|
|
8
|
-
import type {
|
|
8
|
+
import type { RenderSelectionUiRequest } from "@canva/intents/data";
|
|
9
9
|
|
|
10
|
-
export const App = ({
|
|
11
|
-
dataParams,
|
|
12
|
-
}: {
|
|
13
|
-
dataParams: RenderSelectionUiParams;
|
|
14
|
-
}) => (
|
|
10
|
+
export const App = ({ request }: { request: RenderSelectionUiRequest }) => (
|
|
15
11
|
<AppI18nProvider>
|
|
16
12
|
<AppUiProvider>
|
|
17
13
|
<ErrorBoundary fallback={<ErrorPage />}>
|
|
18
|
-
<ContextProvider
|
|
14
|
+
<ContextProvider renderSelectionUiRequest={request}>
|
|
19
15
|
<RouterProvider router={createHashRouter(routes)} />
|
|
20
16
|
</ContextProvider>
|
|
21
17
|
</ErrorBoundary>
|
|
@@ -78,3 +78,22 @@ export const sortOrderField = defineMessages({
|
|
|
78
78
|
description: "Option for sort order",
|
|
79
79
|
},
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
export const filterMenu = defineMessages({
|
|
83
|
+
search: {
|
|
84
|
+
defaultMessage: "Search",
|
|
85
|
+
description: "Label for a search input field",
|
|
86
|
+
},
|
|
87
|
+
clear: {
|
|
88
|
+
defaultMessage: "Clear all",
|
|
89
|
+
description: "Label for a button to clear all filters",
|
|
90
|
+
},
|
|
91
|
+
apply: {
|
|
92
|
+
defaultMessage: "Apply",
|
|
93
|
+
description: "Label for a button to apply filters",
|
|
94
|
+
},
|
|
95
|
+
count: {
|
|
96
|
+
defaultMessage: "Filter count",
|
|
97
|
+
description: "Label for the number of active filters",
|
|
98
|
+
},
|
|
99
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Badge,
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
Column,
|
|
6
|
+
Columns,
|
|
7
|
+
Flyout,
|
|
8
|
+
Rows,
|
|
9
|
+
SearchInputMenu,
|
|
10
|
+
SlidersIcon,
|
|
11
|
+
} from "@canva/app-ui-kit";
|
|
12
|
+
import { useState } from "react";
|
|
13
|
+
import { useIntl } from "react-intl";
|
|
14
|
+
import { filterMenu } from "./messages";
|
|
15
|
+
|
|
16
|
+
interface SearchFilterProps {
|
|
17
|
+
value: string;
|
|
18
|
+
onChange: (value: string) => void;
|
|
19
|
+
filterCount: number;
|
|
20
|
+
resetFilters: () => void;
|
|
21
|
+
children?: React.ReactNode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const SearchFilter = ({
|
|
25
|
+
value,
|
|
26
|
+
onChange,
|
|
27
|
+
filterCount,
|
|
28
|
+
resetFilters,
|
|
29
|
+
children,
|
|
30
|
+
}: SearchFilterProps) => {
|
|
31
|
+
const intl = useIntl();
|
|
32
|
+
const [triggerRef, setTriggerRef] = useState<HTMLDivElement | null>(null);
|
|
33
|
+
const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false);
|
|
34
|
+
|
|
35
|
+
const onFilterClick = () => {
|
|
36
|
+
setIsFilterMenuOpen(!isFilterMenuOpen);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const filterButton = (
|
|
40
|
+
<Button
|
|
41
|
+
size="small"
|
|
42
|
+
variant="tertiary"
|
|
43
|
+
icon={SlidersIcon}
|
|
44
|
+
onClick={onFilterClick}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<Box paddingStart="0.5u">
|
|
50
|
+
<SearchInputMenu
|
|
51
|
+
value={value}
|
|
52
|
+
placeholder={intl.formatMessage(filterMenu.search)}
|
|
53
|
+
onChange={(value) => onChange(value)}
|
|
54
|
+
onClear={() => onChange("")}
|
|
55
|
+
ref={setTriggerRef}
|
|
56
|
+
end={
|
|
57
|
+
filterCount === 0 ? (
|
|
58
|
+
filterButton
|
|
59
|
+
) : (
|
|
60
|
+
<Badge
|
|
61
|
+
tone="assist"
|
|
62
|
+
wrapInset="0"
|
|
63
|
+
shape="circle"
|
|
64
|
+
text={filterCount.toString()}
|
|
65
|
+
ariaLabel={intl.formatMessage(filterMenu.count)}
|
|
66
|
+
>
|
|
67
|
+
{filterButton}
|
|
68
|
+
</Badge>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
/>
|
|
72
|
+
</Box>
|
|
73
|
+
<Flyout
|
|
74
|
+
open={isFilterMenuOpen}
|
|
75
|
+
onRequestClose={() => setIsFilterMenuOpen(false)}
|
|
76
|
+
width="trigger"
|
|
77
|
+
trigger={triggerRef}
|
|
78
|
+
placement="bottom-center"
|
|
79
|
+
footer={
|
|
80
|
+
<Box padding="2u" background="surface">
|
|
81
|
+
<Columns spacing="1u">
|
|
82
|
+
<Column>
|
|
83
|
+
<Button variant="secondary" onClick={resetFilters} stretch>
|
|
84
|
+
{intl.formatMessage(filterMenu.clear)}
|
|
85
|
+
</Button>
|
|
86
|
+
</Column>
|
|
87
|
+
<Column>
|
|
88
|
+
<Button
|
|
89
|
+
variant="primary"
|
|
90
|
+
onClick={() => {
|
|
91
|
+
setIsFilterMenuOpen(false);
|
|
92
|
+
}}
|
|
93
|
+
stretch
|
|
94
|
+
>
|
|
95
|
+
{intl.formatMessage(filterMenu.apply)}
|
|
96
|
+
</Button>
|
|
97
|
+
</Column>
|
|
98
|
+
</Columns>
|
|
99
|
+
</Box>
|
|
100
|
+
}
|
|
101
|
+
>
|
|
102
|
+
<Box padding="2u">
|
|
103
|
+
<Rows spacing="2u">{children}</Rows>
|
|
104
|
+
</Box>
|
|
105
|
+
</Flyout>
|
|
106
|
+
</>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useState, useCallback } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { RenderSelectionUiRequest } from "@canva/intents/data";
|
|
3
3
|
import type { AccessTokenResponse } from "@canva/user";
|
|
4
4
|
import { auth } from "@canva/user";
|
|
5
5
|
import type { APIResponseItem, DataSourceHandler } from "src/api/data_source";
|
|
@@ -8,7 +8,7 @@ import { type DataSourceConfig } from "src/api/data_source";
|
|
|
8
8
|
export interface AppContextType {
|
|
9
9
|
appError: string;
|
|
10
10
|
setAppError: (value: string) => void;
|
|
11
|
-
|
|
11
|
+
request: RenderSelectionUiRequest;
|
|
12
12
|
isAuthenticated: boolean;
|
|
13
13
|
accessToken: AccessTokenResponse | undefined;
|
|
14
14
|
setAccessToken: (token: AccessTokenResponse | undefined) => void;
|
|
@@ -27,7 +27,7 @@ export interface AppContextType {
|
|
|
27
27
|
export const AppContext = createContext<AppContextType>({
|
|
28
28
|
appError: "",
|
|
29
29
|
setAppError: () => {},
|
|
30
|
-
|
|
30
|
+
request: {} as RenderSelectionUiRequest,
|
|
31
31
|
isAuthenticated: false,
|
|
32
32
|
accessToken: undefined,
|
|
33
33
|
setAccessToken: () => {},
|
|
@@ -51,15 +51,15 @@ export const AppContext = createContext<AppContextType>({
|
|
|
51
51
|
* For more information on React Context, refer to the official React documentation: {@link https://react.dev/learn/passing-data-deeply-with-context}.
|
|
52
52
|
*/
|
|
53
53
|
export const ContextProvider = ({
|
|
54
|
-
|
|
54
|
+
renderSelectionUiRequest,
|
|
55
55
|
children,
|
|
56
56
|
}: {
|
|
57
|
-
|
|
57
|
+
renderSelectionUiRequest: RenderSelectionUiRequest;
|
|
58
58
|
children: React.ReactNode;
|
|
59
59
|
}): JSX.Element => {
|
|
60
60
|
const [appError, setAppError] = useState<string>("");
|
|
61
|
-
const [
|
|
62
|
-
|
|
61
|
+
const [request] = useState<RenderSelectionUiRequest>(
|
|
62
|
+
renderSelectionUiRequest,
|
|
63
63
|
);
|
|
64
64
|
|
|
65
65
|
// authentication
|
|
@@ -77,7 +77,7 @@ export const ContextProvider = ({
|
|
|
77
77
|
// data connection
|
|
78
78
|
const loadDataSource = useCallback(
|
|
79
79
|
async (title: string, source: DataSourceConfig) => {
|
|
80
|
-
const result = await
|
|
80
|
+
const result = await request.updateDataRef({
|
|
81
81
|
title,
|
|
82
82
|
source: JSON.stringify(source),
|
|
83
83
|
});
|
|
@@ -91,7 +91,7 @@ export const ContextProvider = ({
|
|
|
91
91
|
setAppError("");
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
|
-
[
|
|
94
|
+
[request],
|
|
95
95
|
);
|
|
96
96
|
|
|
97
97
|
const logout = useCallback(async () => {
|
|
@@ -107,7 +107,7 @@ export const ContextProvider = ({
|
|
|
107
107
|
const value: AppContextType = {
|
|
108
108
|
appError,
|
|
109
109
|
setAppError,
|
|
110
|
-
|
|
110
|
+
request,
|
|
111
111
|
isAuthenticated,
|
|
112
112
|
accessToken,
|
|
113
113
|
setAccessToken,
|
|
@@ -26,27 +26,24 @@ const parseDataSource = (source: string) => {
|
|
|
26
26
|
export const Entrypoint = () => {
|
|
27
27
|
const navigate = useNavigate();
|
|
28
28
|
const context = useAppContext();
|
|
29
|
-
const {
|
|
29
|
+
const { request, setAppError, setDataSourceHandler } = context;
|
|
30
30
|
|
|
31
31
|
useEffect(() => {
|
|
32
32
|
// if the app was loaded with a populated data ref, we should reload the previous state.
|
|
33
33
|
// otherwise, if this is a first launch or there is an error, we should navigate to the first screen for a user - selecting a data source
|
|
34
34
|
let navigateTo: Paths | undefined;
|
|
35
35
|
|
|
36
|
-
if (isDataRefEmpty(
|
|
36
|
+
if (isDataRefEmpty(request)) {
|
|
37
37
|
// probably a first time launch - the user will need to select a data source
|
|
38
38
|
navigateTo = Paths.DATA_SOURCE_SELECTION;
|
|
39
|
-
} else if (
|
|
40
|
-
isOutdatedSource(dataParams) ||
|
|
41
|
-
isLaunchedWithError(dataParams)
|
|
42
|
-
) {
|
|
39
|
+
} else if (isOutdatedSource(request) || isLaunchedWithError(request)) {
|
|
43
40
|
// the configured source does not match the expected data source types
|
|
44
41
|
// so prompt the user to reconfigure the data source
|
|
45
42
|
setAppError("The data source configuration needs to be updated.");
|
|
46
43
|
navigateTo = Paths.DATA_SOURCE_SELECTION;
|
|
47
44
|
} else {
|
|
48
45
|
// there is a data ref, so we should parse it and navigate to the appropriate page
|
|
49
|
-
const dataRef =
|
|
46
|
+
const dataRef = request.invocationContext.dataSourceRef;
|
|
50
47
|
const parsedSource = parseDataSource(dataRef?.source ?? "");
|
|
51
48
|
|
|
52
49
|
if (parsedSource) {
|
|
@@ -67,7 +64,7 @@ export const Entrypoint = () => {
|
|
|
67
64
|
}
|
|
68
65
|
|
|
69
66
|
navigate(navigateTo || Paths.DATA_SOURCE_SELECTION);
|
|
70
|
-
}, [
|
|
67
|
+
}, [request]);
|
|
71
68
|
|
|
72
69
|
return <LoadingIndicator />;
|
|
73
70
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
RenderSelectionUiRequest,
|
|
3
|
+
GetDataTableRequest,
|
|
4
|
+
GetDataTableResponse,
|
|
5
5
|
} from "@canva/intents/data";
|
|
6
6
|
import { prepareDataConnector } from "@canva/intents/data";
|
|
7
7
|
import { createRoot } from "react-dom/client";
|
|
@@ -19,15 +19,15 @@ prepareDataConnector({
|
|
|
19
19
|
*
|
|
20
20
|
* This action is called in two scenarios:
|
|
21
21
|
*
|
|
22
|
-
* - During data selection to preview data before import (when {@link
|
|
22
|
+
* - During data selection to preview data before import (when {@link RenderSelectionUiRequest.updateDataRef} is called).
|
|
23
23
|
* - When refreshing previously imported data (when the user requests an update).
|
|
24
24
|
*
|
|
25
25
|
* @param params - Parameters for the data fetching operation.
|
|
26
26
|
* @returns A promise resolving to either a successful result with data or an error.
|
|
27
27
|
*/
|
|
28
|
-
|
|
29
|
-
params:
|
|
30
|
-
): Promise<
|
|
28
|
+
getDataTable: async (
|
|
29
|
+
params: GetDataTableRequest,
|
|
30
|
+
): Promise<GetDataTableResponse> => {
|
|
31
31
|
const oauth = auth.initOauth();
|
|
32
32
|
const token = await oauth.getAccessToken({ scope });
|
|
33
33
|
return buildDataTableResult(params, token?.token);
|
|
@@ -39,12 +39,12 @@ prepareDataConnector({
|
|
|
39
39
|
* When selection is complete, the implementation must call the `updateDataRef`
|
|
40
40
|
* callback provided in the params to preview and confirm the data selection.
|
|
41
41
|
*
|
|
42
|
-
* @param
|
|
42
|
+
* @param request - parameters that provide context and configuration for the data selection UI.
|
|
43
43
|
* Contains invocation context, size limits, and the updateDataRef callback
|
|
44
44
|
*/
|
|
45
|
-
renderSelectionUi: async (
|
|
45
|
+
renderSelectionUi: async (request: RenderSelectionUiRequest) => {
|
|
46
46
|
function render() {
|
|
47
|
-
root.render(<App
|
|
47
|
+
root.render(<App request={request} />);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
render();
|