@canva/cli 0.0.1-beta.28 → 0.0.1-beta.29

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.
Files changed (50) hide show
  1. package/cli.js +257 -257
  2. package/package.json +1 -1
  3. package/templates/data_connector/README.md +84 -0
  4. package/templates/data_connector/declarations/declarations.d.ts +29 -0
  5. package/templates/data_connector/eslint.config.mjs +14 -0
  6. package/templates/data_connector/package.json +91 -0
  7. package/templates/data_connector/scripts/copy_env.ts +10 -0
  8. package/templates/data_connector/scripts/ssl/ssl.ts +131 -0
  9. package/templates/data_connector/scripts/start/app_runner.ts +201 -0
  10. package/templates/data_connector/scripts/start/context.ts +171 -0
  11. package/templates/data_connector/scripts/start/start.ts +46 -0
  12. package/templates/data_connector/scripts/start/tests/start.tests.ts +61 -0
  13. package/templates/data_connector/src/api/connect_client.ts +6 -0
  14. package/templates/data_connector/src/api/data_source.ts +96 -0
  15. package/templates/data_connector/src/api/data_sources/designs.tsx +253 -0
  16. package/templates/data_connector/src/api/data_sources/index.ts +4 -0
  17. package/templates/data_connector/src/api/data_sources/templates.tsx +287 -0
  18. package/templates/data_connector/src/api/fetch_data_table.ts +51 -0
  19. package/templates/data_connector/src/api/index.ts +4 -0
  20. package/templates/data_connector/src/api/oauth.ts +8 -0
  21. package/templates/data_connector/src/api/tests/data_source.test.tsx +99 -0
  22. package/templates/data_connector/src/app.tsx +24 -0
  23. package/templates/data_connector/src/components/app_error.tsx +15 -0
  24. package/templates/data_connector/src/components/footer.tsx +26 -0
  25. package/templates/data_connector/src/components/header.tsx +40 -0
  26. package/templates/data_connector/src/components/index.ts +3 -0
  27. package/templates/data_connector/src/components/inputs/messages.tsx +80 -0
  28. package/templates/data_connector/src/components/inputs/select_field.tsx +26 -0
  29. package/templates/data_connector/src/context/app_context.tsx +124 -0
  30. package/templates/data_connector/src/context/index.ts +2 -0
  31. package/templates/data_connector/src/context/use_app_context.ts +17 -0
  32. package/templates/data_connector/src/entrypoint.tsx +73 -0
  33. package/templates/data_connector/src/home.tsx +21 -0
  34. package/templates/data_connector/src/index.tsx +69 -0
  35. package/templates/data_connector/src/pages/data_source_config.tsx +9 -0
  36. package/templates/data_connector/src/pages/error.tsx +37 -0
  37. package/templates/data_connector/src/pages/index.ts +4 -0
  38. package/templates/data_connector/src/pages/login.tsx +145 -0
  39. package/templates/data_connector/src/pages/select_source.tsx +24 -0
  40. package/templates/data_connector/src/routes/index.ts +2 -0
  41. package/templates/data_connector/src/routes/protected_route.tsx +25 -0
  42. package/templates/data_connector/src/routes/routes.tsx +46 -0
  43. package/templates/data_connector/src/utils/data_params.ts +17 -0
  44. package/templates/data_connector/src/utils/data_table.ts +100 -0
  45. package/templates/data_connector/src/utils/fetch_result.ts +36 -0
  46. package/templates/data_connector/src/utils/index.ts +2 -0
  47. package/templates/data_connector/src/utils/tests/data_table.test.ts +133 -0
  48. package/templates/data_connector/styles/components.css +38 -0
  49. package/templates/data_connector/tsconfig.json +54 -0
  50. package/templates/data_connector/webpack.config.ts +270 -0
@@ -0,0 +1,287 @@
1
+ /* eslint-disable formatjs/no-literal-string-in-object */
2
+ import { useAppContext } from "src/context";
3
+ import type { APIResponseItem, DataSourceConfig } from "../data_source";
4
+ import { DataAPIError, DataSourceHandler } from "../data_source";
5
+ import type { CanvaItemResponse } from "../connect_client";
6
+ import { dateCell, stringCell } from "src/utils";
7
+ import type { SelectOption } from "@canva/app-ui-kit";
8
+ import {
9
+ Button,
10
+ HorizontalCard,
11
+ Rows,
12
+ TableMergedHeaderCellsIcon,
13
+ } from "@canva/app-ui-kit";
14
+ import { useIntl } from "react-intl";
15
+ import {
16
+ datasetFilter,
17
+ ownershipFilter,
18
+ sortOrderField,
19
+ } from "src/components/inputs/messages";
20
+ import { SelectField } from "src/components/inputs/select_field";
21
+ import { Header } from "src/components";
22
+ import { useNavigate } from "react-router-dom";
23
+ import { Paths } from "src/routes";
24
+ import { useState } from "react";
25
+
26
+ export interface BrandTemplatesDataSource extends DataSourceConfig {
27
+ dataset: "any" | "non_empty" | "empty";
28
+ ownership: "any" | "owned" | "shared";
29
+ sort_by:
30
+ | "relevance"
31
+ | "modified_descending"
32
+ | "modified_ascending"
33
+ | "title_descending"
34
+ | "title_ascending";
35
+ }
36
+
37
+ export interface CanvaBrandTemplate extends APIResponseItem {
38
+ title: string;
39
+ created_at: number;
40
+ updated_at: number;
41
+ view_url: string;
42
+ create_url: string;
43
+ }
44
+
45
+ export const brandTemplatesSource = new DataSourceHandler<
46
+ BrandTemplatesDataSource,
47
+ CanvaBrandTemplate
48
+ >(
49
+ {
50
+ schema: "brand_templates/v1",
51
+ dataset: "any",
52
+ ownership: "any",
53
+ sort_by: "relevance",
54
+ },
55
+ [
56
+ {
57
+ label: "ID",
58
+ getValue: (template: CanvaBrandTemplate) => `ID ${template.id}`,
59
+ toCell: stringCell,
60
+ },
61
+ {
62
+ label: "Title",
63
+ getValue: "title",
64
+ toCell: stringCell,
65
+ },
66
+ {
67
+ label: "Created At",
68
+ getValue: "created_at",
69
+ toCell: dateCell,
70
+ },
71
+ {
72
+ label: "Updated At",
73
+ getValue: "updated_at",
74
+ toCell: dateCell,
75
+ },
76
+ {
77
+ label: "View URL",
78
+ getValue: "view_url",
79
+ toCell: stringCell,
80
+ },
81
+ {
82
+ label: "Create URL",
83
+ getValue: "create_url",
84
+ toCell: stringCell,
85
+ },
86
+ ],
87
+ (
88
+ source: BrandTemplatesDataSource,
89
+ authToken: string,
90
+ rowLimit: number,
91
+ signal: AbortSignal | undefined,
92
+ ) =>
93
+ getBrandTemplates(
94
+ authToken,
95
+ rowLimit,
96
+ signal,
97
+ source.dataset,
98
+ source.ownership,
99
+ source.sort_by,
100
+ ),
101
+ BrandTemplatesSelection,
102
+ BrandTemplatesSourceConfig,
103
+ );
104
+
105
+ export async function getBrandTemplates(
106
+ authToken: string,
107
+ rowLimit: number,
108
+ signal: AbortSignal | undefined,
109
+ ownership: string,
110
+ dataset: string,
111
+ sort_by: string,
112
+ continuation?: string,
113
+ allItems: CanvaBrandTemplate[] = [],
114
+ ): Promise<CanvaBrandTemplate[]> {
115
+ const baseUrl = `https://api.canva.com/rest/v1/brand-templates`;
116
+ const url = continuation
117
+ ? `${baseUrl}?continuation=${continuation}`
118
+ : `${baseUrl}?ownership=${ownership}&dataset=${dataset}&sort_by=${sort_by}`;
119
+
120
+ return fetch(url, {
121
+ headers: {
122
+ Authorization: `Bearer ${authToken}`,
123
+ },
124
+ signal,
125
+ })
126
+ .then((response) => {
127
+ if (!response.ok) {
128
+ throw new DataAPIError(
129
+ `Canva Connect response was not ok: ${response.statusText || response.status}`,
130
+ );
131
+ }
132
+ return response.json();
133
+ })
134
+ .then((data: CanvaItemResponse<CanvaBrandTemplate>) => {
135
+ const updatedItems = [...allItems, ...data.items];
136
+
137
+ if (data.continuation && updatedItems.length < rowLimit) {
138
+ return getBrandTemplates(
139
+ authToken,
140
+ rowLimit,
141
+ signal,
142
+ ownership,
143
+ dataset,
144
+ sort_by,
145
+ data.continuation,
146
+ updatedItems,
147
+ );
148
+ }
149
+
150
+ return updatedItems;
151
+ });
152
+ }
153
+
154
+ function BrandTemplatesSelection() {
155
+ const intl = useIntl();
156
+ const { setDataSourceHandler } = useAppContext();
157
+ const navigate = useNavigate();
158
+
159
+ const title = intl.formatMessage({
160
+ defaultMessage: "Brand Templates",
161
+ description:
162
+ "Main heading on the brand templates button displayed when selecting the import type.",
163
+ });
164
+
165
+ const description = intl.formatMessage({
166
+ defaultMessage: "Query brand templates",
167
+ description:
168
+ "Subtext on the brand templates button displayed when selecting the import type.",
169
+ });
170
+
171
+ const handleClick = () => {
172
+ setDataSourceHandler(
173
+ brandTemplatesSource as unknown as DataSourceHandler<
174
+ DataSourceConfig,
175
+ APIResponseItem
176
+ >,
177
+ );
178
+ navigate(Paths.DATA_SOURCE_CONFIG);
179
+ };
180
+
181
+ return (
182
+ <HorizontalCard
183
+ key="brand-templates"
184
+ title={title}
185
+ thumbnail={{ icon: () => <TableMergedHeaderCellsIcon /> }}
186
+ onClick={handleClick}
187
+ description={description}
188
+ ariaLabel={description}
189
+ />
190
+ );
191
+ }
192
+
193
+ function BrandTemplatesSourceConfig(sourceConfig: BrandTemplatesDataSource) {
194
+ const { loadDataSource } = useAppContext();
195
+ const [ownership, setOwnership] = useState<string>(sourceConfig.ownership);
196
+ const [sortOrder, setSortOrder] = useState<string>(sourceConfig.sort_by);
197
+ const [dataset, setDataset] = useState<string>(sourceConfig.dataset);
198
+ const [isLoading, setIsLoading] = useState(false);
199
+ const intl = useIntl();
200
+
201
+ const loadTemplates = async () => {
202
+ loadDataSource("Canva Brand Templates", {
203
+ schema: "brand_templates/v1",
204
+ ownership,
205
+ dataset,
206
+ sort_by: sortOrder,
207
+ } as BrandTemplatesDataSource).then(() => {
208
+ setIsLoading(false);
209
+ });
210
+ };
211
+
212
+ const ownershipOptions: SelectOption<string>[] = [
213
+ { value: "any", label: intl.formatMessage(ownershipFilter.any) },
214
+ { value: "owned", label: intl.formatMessage(ownershipFilter.owned) },
215
+ { value: "shared", label: intl.formatMessage(ownershipFilter.shared) },
216
+ ];
217
+
218
+ const sortOrderOptions: SelectOption<string>[] = [
219
+ { value: "relevance", label: intl.formatMessage(sortOrderField.relevance) },
220
+ {
221
+ value: "modified_descending",
222
+ label: intl.formatMessage(sortOrderField.modifiedDesc),
223
+ },
224
+ {
225
+ value: "modified_ascending",
226
+ label: intl.formatMessage(sortOrderField.modifiedAsc),
227
+ },
228
+ {
229
+ value: "title_descending",
230
+ label: intl.formatMessage(sortOrderField.titleDesc),
231
+ },
232
+ {
233
+ value: "title_ascending",
234
+ label: intl.formatMessage(sortOrderField.titleAsc),
235
+ },
236
+ ];
237
+
238
+ const datasetOptions: SelectOption<string>[] = [
239
+ { value: "any", label: intl.formatMessage(datasetFilter.any) },
240
+ { value: "non_empty", label: intl.formatMessage(datasetFilter.nonEmpty) },
241
+ { value: "empty", label: intl.formatMessage(datasetFilter.empty) },
242
+ ];
243
+
244
+ return (
245
+ <div>
246
+ <Header
247
+ title={intl.formatMessage({
248
+ defaultMessage: "Canva Brand Templates",
249
+ description: "The header text for the brand templates data source",
250
+ })}
251
+ showBack={true}
252
+ />
253
+ <Rows spacing="2u">
254
+ <SelectField
255
+ label={intl.formatMessage(ownershipFilter.label)}
256
+ options={ownershipOptions}
257
+ value={ownership}
258
+ onChange={setOwnership}
259
+ />
260
+ <SelectField
261
+ label={intl.formatMessage(datasetFilter.label)}
262
+ options={datasetOptions}
263
+ value={dataset}
264
+ onChange={setDataset}
265
+ />
266
+ <SelectField
267
+ label={intl.formatMessage(sortOrderField.label)}
268
+ options={sortOrderOptions}
269
+ value={sortOrder}
270
+ onChange={setSortOrder}
271
+ />
272
+ <Button
273
+ variant="primary"
274
+ loading={isLoading}
275
+ onClick={async () => {
276
+ loadTemplates();
277
+ }}
278
+ >
279
+ {intl.formatMessage({
280
+ defaultMessage: "Load Templates",
281
+ description: "Button for saving and applying the query filter",
282
+ })}
283
+ </Button>
284
+ </Rows>
285
+ </div>
286
+ );
287
+ }
@@ -0,0 +1,51 @@
1
+ import type {
2
+ FetchDataTableParams,
3
+ FetchDataTableResult,
4
+ } from "@canva/intents/data";
5
+ import { DATA_SOURCES } from "./data_sources";
6
+ import {
7
+ appError,
8
+ completeDataTable,
9
+ outdatedSourceRef,
10
+ remoteRequestFailed,
11
+ } from "src/utils/fetch_result";
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 fetchParams
17
+ * @param authToken
18
+ * @returns
19
+ */
20
+ export const buildDataTableResult = async (
21
+ fetchParams: FetchDataTableParams,
22
+ authToken?: string,
23
+ ): Promise<FetchDataTableResult> => {
24
+ const source = JSON.parse(fetchParams.dataSourceRef.source);
25
+ const rowLimit = fetchParams.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
+ fetchParams.signal,
41
+ );
42
+ return completeDataTable(dataTable);
43
+ } catch (error) {
44
+ if (error instanceof DataAPIError) {
45
+ return remoteRequestFailed();
46
+ }
47
+ return appError(
48
+ error instanceof Error ? error.message : "An unknown error occurred",
49
+ );
50
+ }
51
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./connect_client";
2
+ export * from "./fetch_data_table";
3
+ export * from "./data_source";
4
+ export * from "./oauth";
@@ -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 type { DataSourceConfig, APIResponseItem } from "../data_source";
4
+ import { DataSourceHandler, DataAPIError } from "../data_source";
5
+ import { toDataTable } from "src/utils";
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,24 @@
1
+ import { createHashRouter, RouterProvider } from "react-router-dom";
2
+ import { ContextProvider } from "./context";
3
+ import { routes } from "./routes";
4
+ import { AppUiProvider } from "@canva/app-ui-kit";
5
+ import { AppI18nProvider } from "@canva/app-i18n-kit";
6
+ import { ErrorBoundary } from "react-error-boundary";
7
+ import { ErrorPage } from "./pages";
8
+ import type { RenderSelectionUiParams } from "@canva/intents/data";
9
+
10
+ export const App = ({
11
+ dataParams,
12
+ }: {
13
+ dataParams: RenderSelectionUiParams;
14
+ }) => (
15
+ <AppI18nProvider>
16
+ <AppUiProvider>
17
+ <ErrorBoundary fallback={<ErrorPage />}>
18
+ <ContextProvider renderSelectionUiParams={dataParams}>
19
+ <RouterProvider router={createHashRouter(routes)} />
20
+ </ContextProvider>
21
+ </ErrorBoundary>
22
+ </AppUiProvider>
23
+ </AppI18nProvider>
24
+ );
@@ -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 { Rows, Button } 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 { useNavigate } from "react-router-dom";
2
+ import {
3
+ ArrowLeftIcon,
4
+ Box,
5
+ Button,
6
+ Column,
7
+ Columns,
8
+ Title,
9
+ } from "@canva/app-ui-kit";
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
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./app_error";
2
+ export * from "./footer";
3
+ export * from "./header";
@@ -0,0 +1,80 @@
1
+ import { defineMessages } from "react-intl";
2
+
3
+ export const ownershipFilter = defineMessages({
4
+ label: {
5
+ defaultMessage: "Filter by Ownership",
6
+ description: "Label for a select control to filter designs by ownership",
7
+ },
8
+ description: {
9
+ defaultMessage:
10
+ "Filter the list of designs based on the user's ownership of the designs",
11
+ description: "Subtitle for a select control to filter designs by ownership",
12
+ },
13
+ any: {
14
+ defaultMessage: "Any (default)",
15
+ description: "Default option for ownership filter",
16
+ },
17
+ owned: {
18
+ defaultMessage: "Owned by me",
19
+ description: "Option for ownership filter",
20
+ },
21
+ shared: {
22
+ defaultMessage: "Shared with me",
23
+ description: "Option for ownership filter",
24
+ },
25
+ });
26
+
27
+ export const datasetFilter = defineMessages({
28
+ label: {
29
+ defaultMessage: "Filter by Dataset",
30
+ description: "Label for a select control to filter designs by dataset",
31
+ },
32
+ description: {
33
+ defaultMessage:
34
+ "Filter the list of brand templates based on the dataset definitions",
35
+ description: "Subtitle for a select control to filter designs by dataset",
36
+ },
37
+ any: {
38
+ defaultMessage: "Any (default)",
39
+ description: "Default option for dataset filter",
40
+ },
41
+ nonEmpty: {
42
+ defaultMessage: "Templates with one or more data fields defined",
43
+ description: "Option for dataset filter",
44
+ },
45
+ empty: {
46
+ defaultMessage: "Templates with no data fields defined",
47
+ description: "Option for dataset filter",
48
+ },
49
+ });
50
+
51
+ export const sortOrderField = defineMessages({
52
+ label: {
53
+ defaultMessage: "Sort by",
54
+ description: "Label for a select control to sort designs",
55
+ },
56
+ description: {
57
+ defaultMessage: "Sort the list of designs",
58
+ description: "Subtitle for a select control to sort designs",
59
+ },
60
+ relevance: {
61
+ defaultMessage: "Relevance (default)",
62
+ description: "Default option for sort order",
63
+ },
64
+ modifiedDesc: {
65
+ defaultMessage: "Last modified - descending",
66
+ description: "Option for sort order",
67
+ },
68
+ modifiedAsc: {
69
+ defaultMessage: "Last modified - ascending",
70
+ description: "Option for sort order",
71
+ },
72
+ titleDesc: {
73
+ defaultMessage: "Title - descending",
74
+ description: "Option for sort order",
75
+ },
76
+ titleAsc: {
77
+ defaultMessage: "Title - ascending",
78
+ description: "Option for sort order",
79
+ },
80
+ });
@@ -0,0 +1,26 @@
1
+ import type { SelectOption } from "@canva/app-ui-kit";
2
+ import { FormField, Select } from "@canva/app-ui-kit";
3
+
4
+ interface SelectFieldProps {
5
+ value: string;
6
+ onChange: (value: string) => void;
7
+ label: string;
8
+ options: SelectOption<string>[];
9
+ }
10
+
11
+ export const SelectField = ({
12
+ value,
13
+ onChange,
14
+ label,
15
+ options,
16
+ }: SelectFieldProps) => {
17
+ return (
18
+ <FormField
19
+ label={label}
20
+ value={value}
21
+ control={(props) => (
22
+ <Select {...props} options={options} onChange={onChange} />
23
+ )}
24
+ />
25
+ );
26
+ };