@canva/cli 0.0.1-beta.9 → 1.0.0
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/README.md +131 -31
- package/cli.js +841 -437
- package/lib/cjs/index.cjs +11 -0
- package/lib/esm/index.mjs +11 -0
- package/package.json +19 -4
- package/templates/base/backend/routers/auth.ts +4 -4
- package/templates/base/backend/routers/oauth.ts +3 -3
- package/templates/base/eslint.config.mjs +2 -25
- package/templates/base/package.json +26 -23
- package/templates/base/scripts/ssl/ssl.ts +1 -1
- package/templates/base/scripts/start/app_runner.ts +42 -5
- package/templates/base/scripts/start/context.ts +12 -6
- package/templates/base/scripts/start/start.ts +12 -1
- package/templates/base/scripts/start/tests/start.tests.ts +61 -0
- package/templates/base/utils/backend/base_backend/create.ts +3 -3
- package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +2 -2
- package/templates/base/utils/backend/jwt_middleware/jwt_middleware.ts +4 -4
- package/templates/base/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +1 -1
- package/templates/base/utils/use_add_element.ts +1 -1
- package/templates/base/utils/use_feature_support.ts +1 -1
- package/templates/{gen_ai/webpack.config.cjs → base/webpack.config.ts} +50 -68
- package/templates/common/.env.template +3 -3
- package/templates/common/README.md +16 -17
- package/templates/common/jest.config.mjs +1 -1
- package/templates/common/jest.setup.ts +1 -0
- package/templates/common/utils/backend/base_backend/create.ts +3 -3
- package/templates/common/utils/backend/jwt_middleware/jwt_middleware.ts +4 -4
- package/templates/common/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +1 -1
- package/templates/common/utils/tests/table_wrapper.tests.ts +1 -1
- package/templates/common/utils/use_add_element.ts +1 -1
- package/templates/common/utils/use_feature_support.ts +1 -1
- package/templates/dam/backend/routers/dam.ts +24 -20
- package/templates/dam/backend/server.ts +1 -1
- package/templates/dam/canva-app.json +16 -0
- package/templates/dam/eslint.config.mjs +2 -28
- package/templates/dam/package.json +38 -41
- package/templates/dam/scripts/ssl/ssl.ts +1 -1
- package/templates/dam/scripts/start/app_runner.ts +42 -5
- package/templates/dam/scripts/start/context.ts +12 -6
- package/templates/dam/scripts/start/start.ts +12 -1
- package/templates/dam/scripts/start/tests/start.tests.ts +61 -0
- package/templates/dam/src/app.tsx +24 -3
- package/templates/dam/src/config.ts +212 -87
- package/templates/dam/src/index.tsx +1 -1
- package/templates/dam/utils/backend/base_backend/create.ts +3 -3
- package/templates/dam/utils/backend/jwt_middleware/jwt_middleware.ts +4 -4
- package/templates/dam/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +1 -1
- package/templates/{base/webpack.config.cjs → dam/webpack.config.ts} +50 -68
- package/templates/data_connector/README.md +84 -0
- package/templates/data_connector/canva-app.json +21 -0
- package/templates/data_connector/declarations/declarations.d.ts +29 -0
- package/templates/data_connector/eslint.config.mjs +14 -0
- package/templates/data_connector/package.json +91 -0
- package/templates/data_connector/scripts/ssl/ssl.ts +131 -0
- package/templates/data_connector/scripts/start/app_runner.ts +201 -0
- package/templates/data_connector/scripts/start/context.ts +171 -0
- package/templates/data_connector/scripts/start/start.ts +46 -0
- package/templates/data_connector/scripts/start/tests/start.tests.ts +61 -0
- package/templates/data_connector/src/api/connect_client.ts +6 -0
- package/templates/data_connector/src/api/data_source.ts +96 -0
- package/templates/data_connector/src/api/data_sources/designs.tsx +296 -0
- package/templates/data_connector/src/api/data_sources/index.ts +4 -0
- package/templates/data_connector/src/api/data_sources/templates.tsx +329 -0
- package/templates/data_connector/src/api/fetch_data_table.ts +55 -0
- package/templates/data_connector/src/api/index.ts +4 -0
- package/templates/data_connector/src/api/oauth.ts +8 -0
- package/templates/data_connector/src/api/tests/data_source.test.tsx +99 -0
- package/templates/data_connector/src/app.tsx +20 -0
- package/templates/data_connector/src/components/app_error.tsx +15 -0
- package/templates/data_connector/src/components/footer.tsx +26 -0
- package/templates/data_connector/src/components/header.tsx +40 -0
- package/templates/data_connector/src/components/index.ts +3 -0
- package/templates/data_connector/src/components/inputs/messages.tsx +99 -0
- package/templates/data_connector/src/components/inputs/search_filter.tsx +108 -0
- package/templates/data_connector/src/components/inputs/select_field.tsx +26 -0
- package/templates/data_connector/src/context/app_context.tsx +124 -0
- package/templates/data_connector/src/context/index.ts +2 -0
- package/templates/data_connector/src/context/use_app_context.ts +17 -0
- package/templates/data_connector/src/entrypoint.tsx +70 -0
- package/templates/data_connector/src/home.tsx +21 -0
- package/templates/data_connector/src/index.tsx +68 -0
- package/templates/data_connector/src/pages/data_source_config.tsx +9 -0
- package/templates/data_connector/src/pages/error.tsx +37 -0
- package/templates/data_connector/src/pages/index.ts +4 -0
- package/templates/data_connector/src/pages/login.tsx +145 -0
- package/templates/data_connector/src/pages/select_source.tsx +24 -0
- package/templates/data_connector/src/routes/index.ts +2 -0
- package/templates/data_connector/src/routes/protected_route.tsx +25 -0
- package/templates/data_connector/src/routes/routes.tsx +46 -0
- package/templates/data_connector/src/utils/data_params.ts +17 -0
- package/templates/data_connector/src/utils/data_table.ts +115 -0
- package/templates/data_connector/src/utils/fetch_result.ts +36 -0
- package/templates/data_connector/src/utils/index.ts +2 -0
- package/templates/data_connector/src/utils/tests/data_table.test.ts +133 -0
- package/templates/data_connector/styles/components.css +38 -0
- package/templates/data_connector/tsconfig.json +54 -0
- package/templates/{dam/webpack.config.cjs → data_connector/webpack.config.ts} +50 -68
- package/templates/gen_ai/backend/routers/image.ts +8 -8
- package/templates/gen_ai/backend/server.ts +1 -1
- package/templates/gen_ai/canva-app.json +20 -0
- package/templates/gen_ai/eslint.config.mjs +2 -25
- package/templates/gen_ai/package.json +42 -43
- package/templates/gen_ai/scripts/ssl/ssl.ts +1 -1
- package/templates/gen_ai/scripts/start/app_runner.ts +42 -5
- package/templates/gen_ai/scripts/start/context.ts +12 -6
- package/templates/gen_ai/scripts/start/start.ts +12 -1
- package/templates/gen_ai/scripts/start/tests/start.tests.ts +61 -0
- package/templates/gen_ai/src/app.tsx +3 -3
- package/templates/gen_ai/src/components/footer.tsx +4 -4
- package/templates/gen_ai/src/components/image_grid.tsx +1 -1
- package/templates/gen_ai/src/components/loading_results.tsx +8 -8
- package/templates/gen_ai/src/components/prompt_input.tsx +3 -3
- package/templates/gen_ai/src/components/remaining_credits.tsx +1 -1
- package/templates/gen_ai/src/components/report_box.tsx +1 -1
- package/templates/gen_ai/src/components/tests/remaining_credit.tests.tsx +2 -2
- package/templates/gen_ai/src/home.tsx +2 -2
- package/templates/gen_ai/src/pages/error.tsx +2 -2
- package/templates/gen_ai/src/pages/results.tsx +1 -1
- package/templates/gen_ai/utils/backend/base_backend/create.ts +3 -3
- package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +2 -2
- package/templates/{hello_world/webpack.config.cjs → gen_ai/webpack.config.ts} +50 -68
- package/templates/hello_world/canva-app.json +16 -0
- package/templates/hello_world/eslint.config.mjs +2 -25
- package/templates/hello_world/package.json +35 -38
- package/templates/hello_world/scripts/copy_env.ts +10 -0
- package/templates/hello_world/scripts/ssl/ssl.ts +1 -1
- package/templates/hello_world/scripts/start/app_runner.ts +42 -5
- package/templates/hello_world/scripts/start/context.ts +12 -6
- package/templates/hello_world/scripts/start/start.ts +12 -1
- package/templates/hello_world/scripts/start/tests/start.tests.ts +61 -0
- package/templates/hello_world/src/app.tsx +1 -1
- package/templates/hello_world/src/index.tsx +1 -1
- package/templates/hello_world/src/tests/__snapshots__/app.tests.tsx.snap +5 -5
- package/templates/hello_world/src/tests/app.tests.tsx +3 -3
- package/templates/hello_world/utils/use_add_element.ts +1 -1
- package/templates/hello_world/utils/use_feature_support.ts +1 -1
- package/templates/hello_world/webpack.config.ts +254 -0
- package/templates/optional/.cursor/mcp.json +8 -0
- package/templates/optional/.vscode/mcp.json +9 -0
- package/templates/optional/AGENTS.md +76 -0
- package/templates/optional/CLAUDE.md +76 -0
- package/templates/common/conf/eslint-general.mjs +0 -303
- package/templates/common/conf/eslint-i18n.mjs +0 -41
- package/templates/common/conf/eslint-local-i18n-rules/index.mjs +0 -181
- /package/templates/base/scripts/{copy-env.ts → copy_env.ts} +0 -0
- /package/templates/dam/scripts/{copy-env.ts → copy_env.ts} +0 -0
- /package/templates/{gen_ai/scripts/copy-env.ts → data_connector/scripts/copy_env.ts} +0 -0
- /package/templates/{hello_world/scripts/copy-env.ts → gen_ai/scripts/copy_env.ts} +0 -0
- /package/templates/{common → optional}/.vscode/extensions.json +0 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/* eslint-disable formatjs/no-literal-string-in-object */
|
|
2
|
+
import type { SelectOption } from "@canva/app-ui-kit";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
HorizontalCard,
|
|
6
|
+
Rows,
|
|
7
|
+
TableMergedHeaderCellsIcon,
|
|
8
|
+
} from "@canva/app-ui-kit";
|
|
9
|
+
import { useEffect, useState } from "react";
|
|
10
|
+
import { useIntl } from "react-intl";
|
|
11
|
+
import { useNavigate } from "react-router-dom";
|
|
12
|
+
import { Header } from "src/components";
|
|
13
|
+
import {
|
|
14
|
+
datasetFilter,
|
|
15
|
+
ownershipFilter,
|
|
16
|
+
sortOrderField,
|
|
17
|
+
} from "src/components/inputs/messages";
|
|
18
|
+
import { SearchFilter } from "src/components/inputs/search_filter";
|
|
19
|
+
import { SelectField } from "src/components/inputs/select_field";
|
|
20
|
+
import { useAppContext } from "src/context";
|
|
21
|
+
import { Paths } from "src/routes";
|
|
22
|
+
import { dateCell, stringCell } from "src/utils";
|
|
23
|
+
import type { CanvaItemResponse } from "../connect_client";
|
|
24
|
+
import { DataAPIError, DataSourceHandler } from "../data_source";
|
|
25
|
+
import type { APIResponseItem, DataSourceConfig } from "../data_source";
|
|
26
|
+
|
|
27
|
+
export interface BrandTemplatesDataSource extends DataSourceConfig {
|
|
28
|
+
query: string;
|
|
29
|
+
dataset: "any" | "non_empty" | "empty";
|
|
30
|
+
ownership: "any" | "owned" | "shared";
|
|
31
|
+
sort_by:
|
|
32
|
+
| "relevance"
|
|
33
|
+
| "modified_descending"
|
|
34
|
+
| "modified_ascending"
|
|
35
|
+
| "title_descending"
|
|
36
|
+
| "title_ascending";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CanvaBrandTemplate extends APIResponseItem {
|
|
40
|
+
title: string;
|
|
41
|
+
created_at: number;
|
|
42
|
+
updated_at: number;
|
|
43
|
+
view_url: string;
|
|
44
|
+
create_url: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const brandTemplatesSource = new DataSourceHandler<
|
|
48
|
+
BrandTemplatesDataSource,
|
|
49
|
+
CanvaBrandTemplate
|
|
50
|
+
>(
|
|
51
|
+
{
|
|
52
|
+
schema: "brand_templates/v1",
|
|
53
|
+
query: "",
|
|
54
|
+
dataset: "any",
|
|
55
|
+
ownership: "any",
|
|
56
|
+
sort_by: "relevance",
|
|
57
|
+
},
|
|
58
|
+
[
|
|
59
|
+
{
|
|
60
|
+
label: "ID",
|
|
61
|
+
getValue: (template: CanvaBrandTemplate) => `ID ${template.id}`,
|
|
62
|
+
toCell: stringCell,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: "Title",
|
|
66
|
+
getValue: "title",
|
|
67
|
+
toCell: stringCell,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: "Created At",
|
|
71
|
+
getValue: "created_at",
|
|
72
|
+
toCell: dateCell,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "Updated At",
|
|
76
|
+
getValue: "updated_at",
|
|
77
|
+
toCell: dateCell,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: "View URL",
|
|
81
|
+
getValue: "view_url",
|
|
82
|
+
toCell: stringCell,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
label: "Create URL",
|
|
86
|
+
getValue: "create_url",
|
|
87
|
+
toCell: stringCell,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
(
|
|
91
|
+
source: BrandTemplatesDataSource,
|
|
92
|
+
authToken: string,
|
|
93
|
+
rowLimit: number,
|
|
94
|
+
signal: AbortSignal | undefined,
|
|
95
|
+
) =>
|
|
96
|
+
getBrandTemplates(
|
|
97
|
+
authToken,
|
|
98
|
+
rowLimit,
|
|
99
|
+
signal,
|
|
100
|
+
source.query,
|
|
101
|
+
source.ownership,
|
|
102
|
+
source.dataset,
|
|
103
|
+
source.sort_by,
|
|
104
|
+
),
|
|
105
|
+
BrandTemplatesSelection,
|
|
106
|
+
BrandTemplatesSourceConfig,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
export async function getBrandTemplates(
|
|
110
|
+
authToken: string,
|
|
111
|
+
rowLimit: number,
|
|
112
|
+
signal: AbortSignal | undefined,
|
|
113
|
+
query: string,
|
|
114
|
+
ownership: string,
|
|
115
|
+
dataset: string,
|
|
116
|
+
sort_by: string,
|
|
117
|
+
continuation?: string,
|
|
118
|
+
allItems: CanvaBrandTemplate[] = [],
|
|
119
|
+
): Promise<CanvaBrandTemplate[]> {
|
|
120
|
+
const baseUrl = `https://api.canva.com/rest/v1/brand-templates`;
|
|
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()}`;
|
|
134
|
+
|
|
135
|
+
return fetch(url, {
|
|
136
|
+
headers: {
|
|
137
|
+
Authorization: `Bearer ${authToken}`,
|
|
138
|
+
},
|
|
139
|
+
signal,
|
|
140
|
+
})
|
|
141
|
+
.then((response) => {
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
throw new DataAPIError(
|
|
144
|
+
`Canva Connect response was not ok: ${response.statusText || response.status}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return response.json();
|
|
148
|
+
})
|
|
149
|
+
.then((data: CanvaItemResponse<CanvaBrandTemplate>) => {
|
|
150
|
+
const updatedItems = [...allItems, ...data.items];
|
|
151
|
+
|
|
152
|
+
if (data.continuation && updatedItems.length < rowLimit) {
|
|
153
|
+
return getBrandTemplates(
|
|
154
|
+
authToken,
|
|
155
|
+
rowLimit,
|
|
156
|
+
signal,
|
|
157
|
+
query,
|
|
158
|
+
ownership,
|
|
159
|
+
dataset,
|
|
160
|
+
sort_by,
|
|
161
|
+
data.continuation,
|
|
162
|
+
updatedItems,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return updatedItems;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function BrandTemplatesSelection() {
|
|
171
|
+
const intl = useIntl();
|
|
172
|
+
const { setDataSourceHandler } = useAppContext();
|
|
173
|
+
const navigate = useNavigate();
|
|
174
|
+
|
|
175
|
+
const title = intl.formatMessage({
|
|
176
|
+
defaultMessage: "Brand Templates",
|
|
177
|
+
description:
|
|
178
|
+
"Main heading on the brand templates button displayed when selecting the import type.",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const description = intl.formatMessage({
|
|
182
|
+
defaultMessage: "Query brand templates",
|
|
183
|
+
description:
|
|
184
|
+
"Subtext on the brand templates button displayed when selecting the import type.",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const handleClick = () => {
|
|
188
|
+
setDataSourceHandler(
|
|
189
|
+
brandTemplatesSource as unknown as DataSourceHandler<
|
|
190
|
+
DataSourceConfig,
|
|
191
|
+
APIResponseItem
|
|
192
|
+
>,
|
|
193
|
+
);
|
|
194
|
+
navigate(Paths.DATA_SOURCE_CONFIG);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<HorizontalCard
|
|
199
|
+
key="brand-templates"
|
|
200
|
+
title={title}
|
|
201
|
+
thumbnail={{ icon: () => <TableMergedHeaderCellsIcon /> }}
|
|
202
|
+
onClick={handleClick}
|
|
203
|
+
description={description}
|
|
204
|
+
ariaLabel={description}
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function BrandTemplatesSourceConfig(sourceConfig: BrandTemplatesDataSource) {
|
|
210
|
+
const intl = useIntl();
|
|
211
|
+
const { loadDataSource } = useAppContext();
|
|
212
|
+
const [query, setQuery] = useState<string>(sourceConfig.query);
|
|
213
|
+
const [ownership, setOwnership] = useState<string>(sourceConfig.ownership);
|
|
214
|
+
const [sortOrder, setSortOrder] = useState<string>(sourceConfig.sort_by);
|
|
215
|
+
const [dataset, setDataset] = useState<string>(sourceConfig.dataset);
|
|
216
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
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
|
+
};
|
|
233
|
+
|
|
234
|
+
const loadTemplates = async () => {
|
|
235
|
+
loadDataSource("Canva Brand Templates", {
|
|
236
|
+
schema: "brand_templates/v1",
|
|
237
|
+
query,
|
|
238
|
+
ownership,
|
|
239
|
+
dataset,
|
|
240
|
+
sort_by: sortOrder,
|
|
241
|
+
} as BrandTemplatesDataSource).then(() => {
|
|
242
|
+
setIsLoading(false);
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const ownershipOptions: SelectOption<string>[] = [
|
|
247
|
+
{ value: "any", label: intl.formatMessage(ownershipFilter.any) },
|
|
248
|
+
{ value: "owned", label: intl.formatMessage(ownershipFilter.owned) },
|
|
249
|
+
{ value: "shared", label: intl.formatMessage(ownershipFilter.shared) },
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
const sortOrderOptions: SelectOption<string>[] = [
|
|
253
|
+
{ value: "relevance", label: intl.formatMessage(sortOrderField.relevance) },
|
|
254
|
+
{
|
|
255
|
+
value: "modified_descending",
|
|
256
|
+
label: intl.formatMessage(sortOrderField.modifiedDesc),
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
value: "modified_ascending",
|
|
260
|
+
label: intl.formatMessage(sortOrderField.modifiedAsc),
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
value: "title_descending",
|
|
264
|
+
label: intl.formatMessage(sortOrderField.titleDesc),
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
value: "title_ascending",
|
|
268
|
+
label: intl.formatMessage(sortOrderField.titleAsc),
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
const datasetOptions: SelectOption<string>[] = [
|
|
273
|
+
{ value: "any", label: intl.formatMessage(datasetFilter.any) },
|
|
274
|
+
{ value: "non_empty", label: intl.formatMessage(datasetFilter.nonEmpty) },
|
|
275
|
+
{ value: "empty", label: intl.formatMessage(datasetFilter.empty) },
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div>
|
|
280
|
+
<Header
|
|
281
|
+
title={intl.formatMessage({
|
|
282
|
+
defaultMessage: "Canva Brand Templates",
|
|
283
|
+
description: "The header text for the brand templates data source",
|
|
284
|
+
})}
|
|
285
|
+
showBack={true}
|
|
286
|
+
/>
|
|
287
|
+
<Rows spacing="2u">
|
|
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
|
+
|
|
314
|
+
<Button
|
|
315
|
+
variant="primary"
|
|
316
|
+
loading={isLoading}
|
|
317
|
+
onClick={async () => {
|
|
318
|
+
loadTemplates();
|
|
319
|
+
}}
|
|
320
|
+
>
|
|
321
|
+
{intl.formatMessage({
|
|
322
|
+
defaultMessage: "Load Templates",
|
|
323
|
+
description: "Button for saving and applying the query filter",
|
|
324
|
+
})}
|
|
325
|
+
</Button>
|
|
326
|
+
</Rows>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GetDataTableRequest,
|
|
3
|
+
GetDataTableResponse,
|
|
4
|
+
} from "@canva/intents/data";
|
|
5
|
+
import {
|
|
6
|
+
appError,
|
|
7
|
+
completeDataTable,
|
|
8
|
+
outdatedSourceRef,
|
|
9
|
+
remoteRequestFailed,
|
|
10
|
+
} from "src/utils/fetch_result";
|
|
11
|
+
import { DATA_SOURCES } from "./data_sources";
|
|
12
|
+
import { DataAPIError } from ".";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* This function handles parsing the data fetch parameters and calling the appropriate handler for the data source.
|
|
16
|
+
* @param request
|
|
17
|
+
* @param authToken
|
|
18
|
+
* @returns
|
|
19
|
+
*/
|
|
20
|
+
export const buildDataTableResult = async (
|
|
21
|
+
request: GetDataTableRequest,
|
|
22
|
+
authToken?: string,
|
|
23
|
+
): Promise<GetDataTableResponse> => {
|
|
24
|
+
const source = JSON.parse(request.dataSourceRef.source);
|
|
25
|
+
const rowLimit = request.limit.row - 1; // -1 for the header row
|
|
26
|
+
|
|
27
|
+
const dataHandler = DATA_SOURCES.find((handler) =>
|
|
28
|
+
handler.matchSource(source),
|
|
29
|
+
);
|
|
30
|
+
if (!dataHandler) {
|
|
31
|
+
// the configured source does not match the expected data source type
|
|
32
|
+
// return an error result which will prompt the user to reconfigure the data source
|
|
33
|
+
return outdatedSourceRef();
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const dataTable = await dataHandler.fetchAndBuildTable(
|
|
37
|
+
source,
|
|
38
|
+
authToken || "",
|
|
39
|
+
rowLimit,
|
|
40
|
+
request.signal,
|
|
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
|
+
}
|
|
46
|
+
return completeDataTable(dataTable);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof DataAPIError) {
|
|
49
|
+
return remoteRequestFailed();
|
|
50
|
+
}
|
|
51
|
+
return appError(
|
|
52
|
+
error instanceof Error ? error.message : "An unknown error occurred",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scopes to request as part of the OAuth flow.
|
|
3
|
+
* These scopes are required to read the user's designs and brand templates.
|
|
4
|
+
*
|
|
5
|
+
* @see https://www.canva.dev/docs/apps/authenticating-users/oauth/#overview - for more information about using the @canva/user package to support OAuth login.
|
|
6
|
+
* @see https://www.canva.dev/docs/connect/appendix/scopes/ - for a full list of Canva Connect API scopes.
|
|
7
|
+
*/
|
|
8
|
+
export const scope = new Set(["design:meta:read", "brandtemplate:meta:read"]);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* eslint-disable formatjs/no-literal-string-in-object */
|
|
2
|
+
/* eslint-disable formatjs/no-literal-string-in-jsx */
|
|
3
|
+
import { toDataTable } from "src/utils";
|
|
4
|
+
import type { APIResponseItem, DataSourceConfig } from "../data_source";
|
|
5
|
+
import { DataAPIError, DataSourceHandler } from "../data_source";
|
|
6
|
+
|
|
7
|
+
// Mock dependencies
|
|
8
|
+
jest.mock("src/utils", () => ({
|
|
9
|
+
toDataTable: jest.fn().mockReturnValue({ rows: [] }),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe("DataSourceHandler", () => {
|
|
13
|
+
// Test interface
|
|
14
|
+
interface TestConfig extends DataSourceConfig {
|
|
15
|
+
testField: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface TestResponse extends APIResponseItem {
|
|
19
|
+
name: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Test data
|
|
23
|
+
const mockConfig: TestConfig = {
|
|
24
|
+
schema: "test/v1",
|
|
25
|
+
testField: "value",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mockColumns = [
|
|
29
|
+
{
|
|
30
|
+
label: "ID",
|
|
31
|
+
getValue: (item: TestResponse) => `ID ${item.id}`,
|
|
32
|
+
toCell: jest.fn().mockReturnValue({ type: "string", value: "id-123" }),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: "Name",
|
|
36
|
+
getValue: "name" as const,
|
|
37
|
+
toCell: jest.fn().mockReturnValue({ type: "string", value: "Test Name" }),
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const mockFetchData = jest.fn();
|
|
42
|
+
const mockSelectionPage = jest.fn().mockReturnValue(<div>Selection</div>);
|
|
43
|
+
const mockConfigPage = jest.fn().mockReturnValue(<div>Config</div>);
|
|
44
|
+
|
|
45
|
+
let handler: DataSourceHandler<TestConfig, TestResponse>;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.clearAllMocks();
|
|
49
|
+
handler = new DataSourceHandler<TestConfig, TestResponse>(
|
|
50
|
+
mockConfig,
|
|
51
|
+
mockColumns,
|
|
52
|
+
mockFetchData,
|
|
53
|
+
mockSelectionPage,
|
|
54
|
+
mockConfigPage,
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("creates handler with correct schema", () => {
|
|
59
|
+
expect(handler.schema).toBe("test/v1");
|
|
60
|
+
expect(handler.sourceConfig).toEqual(mockConfig);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("matchSource returns true for matching schema", () => {
|
|
64
|
+
const result = handler.matchSource({ schema: "test/v1" });
|
|
65
|
+
expect(result).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("matchSource returns false for non-matching schema", () => {
|
|
69
|
+
const result = handler.matchSource({ schema: "wrong/v1" });
|
|
70
|
+
expect(result).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("fetchAndBuildTable calls fetchData with correct parameters", async () => {
|
|
74
|
+
const mockData = [{ id: "id-123", name: "Test Item" }];
|
|
75
|
+
mockFetchData.mockResolvedValueOnce(mockData);
|
|
76
|
+
|
|
77
|
+
const authToken = "test-token";
|
|
78
|
+
const rowLimit = 10;
|
|
79
|
+
const signal = new AbortController().signal;
|
|
80
|
+
|
|
81
|
+
await handler.fetchAndBuildTable(mockConfig, authToken, rowLimit, signal);
|
|
82
|
+
|
|
83
|
+
expect(mockFetchData).toHaveBeenCalledWith(
|
|
84
|
+
mockConfig,
|
|
85
|
+
authToken,
|
|
86
|
+
rowLimit,
|
|
87
|
+
signal,
|
|
88
|
+
);
|
|
89
|
+
expect(toDataTable).toHaveBeenCalledWith(mockData, mockColumns, rowLimit);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("fetchAndBuildTable throws DataAPIError when fetchData fails", async () => {
|
|
93
|
+
mockFetchData.mockRejectedValueOnce(new Error("Network error"));
|
|
94
|
+
|
|
95
|
+
await expect(
|
|
96
|
+
handler.fetchAndBuildTable(mockConfig, "token", 10, undefined),
|
|
97
|
+
).rejects.toThrow(DataAPIError);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AppI18nProvider } from "@canva/app-i18n-kit";
|
|
2
|
+
import { AppUiProvider } from "@canva/app-ui-kit";
|
|
3
|
+
import type { RenderSelectionUiRequest } from "@canva/intents/data";
|
|
4
|
+
import { ErrorBoundary } from "react-error-boundary";
|
|
5
|
+
import { createHashRouter, RouterProvider } from "react-router-dom";
|
|
6
|
+
import { ContextProvider } from "./context";
|
|
7
|
+
import { ErrorPage } from "./pages";
|
|
8
|
+
import { routes } from "./routes";
|
|
9
|
+
|
|
10
|
+
export const App = ({ request }: { request: RenderSelectionUiRequest }) => (
|
|
11
|
+
<AppI18nProvider>
|
|
12
|
+
<AppUiProvider>
|
|
13
|
+
<ErrorBoundary fallback={<ErrorPage />}>
|
|
14
|
+
<ContextProvider renderSelectionUiRequest={request}>
|
|
15
|
+
<RouterProvider router={createHashRouter(routes)} />
|
|
16
|
+
</ContextProvider>
|
|
17
|
+
</ErrorBoundary>
|
|
18
|
+
</AppUiProvider>
|
|
19
|
+
</AppI18nProvider>
|
|
20
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Alert } from "@canva/app-ui-kit";
|
|
2
|
+
import { useAppContext } from "src/context";
|
|
3
|
+
|
|
4
|
+
export const AppError = () => {
|
|
5
|
+
const { appError, setAppError } = useAppContext();
|
|
6
|
+
if (!appError) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<Alert tone="critical" onDismiss={() => setAppError("")}>
|
|
12
|
+
{appError}
|
|
13
|
+
</Alert>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Button, Rows } from "@canva/app-ui-kit";
|
|
2
|
+
import { useIntl } from "react-intl";
|
|
3
|
+
import { useAppContext } from "src/context";
|
|
4
|
+
|
|
5
|
+
export const Footer = () => {
|
|
6
|
+
const { isAuthenticated, logout } = useAppContext();
|
|
7
|
+
const intl = useIntl();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Rows spacing="1u">
|
|
11
|
+
{isAuthenticated && (
|
|
12
|
+
<Button
|
|
13
|
+
variant="tertiary"
|
|
14
|
+
onClick={async () => {
|
|
15
|
+
logout();
|
|
16
|
+
}}
|
|
17
|
+
>
|
|
18
|
+
{intl.formatMessage({
|
|
19
|
+
defaultMessage: "Log Out",
|
|
20
|
+
description: "Button for logging out of the data source",
|
|
21
|
+
})}
|
|
22
|
+
</Button>
|
|
23
|
+
)}
|
|
24
|
+
</Rows>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowLeftIcon,
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
Column,
|
|
6
|
+
Columns,
|
|
7
|
+
Title,
|
|
8
|
+
} from "@canva/app-ui-kit";
|
|
9
|
+
import { useNavigate } from "react-router-dom";
|
|
10
|
+
import { Paths } from "src/routes";
|
|
11
|
+
|
|
12
|
+
export const Header = ({
|
|
13
|
+
title,
|
|
14
|
+
showBack,
|
|
15
|
+
}: {
|
|
16
|
+
title: string;
|
|
17
|
+
showBack: boolean;
|
|
18
|
+
}) => {
|
|
19
|
+
const navigate = useNavigate();
|
|
20
|
+
return (
|
|
21
|
+
<Box paddingBottom="1u">
|
|
22
|
+
<Columns spacing="0" alignY="center" align="start">
|
|
23
|
+
{showBack && (
|
|
24
|
+
<Column width="content">
|
|
25
|
+
<Button
|
|
26
|
+
onClick={() => navigate(Paths.DATA_SOURCE_SELECTION)}
|
|
27
|
+
icon={ArrowLeftIcon}
|
|
28
|
+
variant="tertiary"
|
|
29
|
+
/>
|
|
30
|
+
</Column>
|
|
31
|
+
)}
|
|
32
|
+
<Column>
|
|
33
|
+
<Title size="small" lineClamp={1}>
|
|
34
|
+
{title}
|
|
35
|
+
</Title>
|
|
36
|
+
</Column>
|
|
37
|
+
</Columns>
|
|
38
|
+
</Box>
|
|
39
|
+
);
|
|
40
|
+
};
|