@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,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Box,
|
|
3
|
+
Button,
|
|
4
|
+
LoadingIndicator,
|
|
5
|
+
Rows,
|
|
6
|
+
Text,
|
|
7
|
+
Title,
|
|
8
|
+
} from "@canva/app-ui-kit";
|
|
9
|
+
import { useCallback, useEffect, useState } from "react";
|
|
10
|
+
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
|
11
|
+
import { useNavigate } from "react-router-dom";
|
|
12
|
+
import { scope } from "src/api";
|
|
13
|
+
import { Header } from "src/components";
|
|
14
|
+
import * as styles from "styles/components.css";
|
|
15
|
+
import { useAppContext } from "../context";
|
|
16
|
+
import { Paths } from "../routes";
|
|
17
|
+
|
|
18
|
+
export const Login = () => {
|
|
19
|
+
const intl = useIntl();
|
|
20
|
+
const navigate = useNavigate();
|
|
21
|
+
const { oauth, setAccessToken, isAuthenticated } = useAppContext();
|
|
22
|
+
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
const [loading, setLoading] = useState(true);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
// Redirect if already authenticated
|
|
28
|
+
if (isAuthenticated) {
|
|
29
|
+
navigate(Paths.ENTRYPOINT);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// check if the user is already authenticated
|
|
34
|
+
retrieveAndSetToken();
|
|
35
|
+
}, [isAuthenticated]);
|
|
36
|
+
|
|
37
|
+
const authorize = useCallback(async () => {
|
|
38
|
+
setLoading(true);
|
|
39
|
+
setError(null);
|
|
40
|
+
try {
|
|
41
|
+
await oauth.requestAuthorization({ scope });
|
|
42
|
+
await retrieveAndSetToken();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
setError(error instanceof Error ? error.message : "Unknown error");
|
|
45
|
+
setLoading(false);
|
|
46
|
+
}
|
|
47
|
+
}, [oauth]);
|
|
48
|
+
|
|
49
|
+
// you MUST call getAccessToken every time you need a token, as the token may expire.
|
|
50
|
+
// Canva will handle caching and refreshing the token for you.
|
|
51
|
+
const retrieveAndSetToken = useCallback(
|
|
52
|
+
async (forceRefresh = false) => {
|
|
53
|
+
try {
|
|
54
|
+
const token = await oauth.getAccessToken({ forceRefresh, scope });
|
|
55
|
+
setAccessToken(token);
|
|
56
|
+
setLoading(false);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
setError(error instanceof Error ? error.message : "Unknown error");
|
|
59
|
+
setLoading(false);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
[oauth, setAccessToken],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className={styles.scrollContainer}>
|
|
67
|
+
<Box
|
|
68
|
+
justifyContent="center"
|
|
69
|
+
width="full"
|
|
70
|
+
alignItems="center"
|
|
71
|
+
display="flex"
|
|
72
|
+
height="full"
|
|
73
|
+
>
|
|
74
|
+
{error && (
|
|
75
|
+
<Rows spacing="2u">
|
|
76
|
+
<Title>
|
|
77
|
+
<FormattedMessage {...loginMessages.authorizationError} />
|
|
78
|
+
</Title>
|
|
79
|
+
<Text>{error}</Text>
|
|
80
|
+
<Button variant="primary" onClick={authorize}>
|
|
81
|
+
{intl.formatMessage(loginMessages.tryAgain)}
|
|
82
|
+
</Button>
|
|
83
|
+
</Rows>
|
|
84
|
+
)}
|
|
85
|
+
{loading && <LoadingIndicator />}
|
|
86
|
+
{!loading && !error && (
|
|
87
|
+
<Rows spacing="2u">
|
|
88
|
+
<Header
|
|
89
|
+
title={intl.formatMessage(loginMessages.signInRequired)}
|
|
90
|
+
showBack={false}
|
|
91
|
+
/>
|
|
92
|
+
<Text>
|
|
93
|
+
<FormattedMessage {...loginMessages.dataConnectorsOAuth} />
|
|
94
|
+
</Text>
|
|
95
|
+
<Text>
|
|
96
|
+
<FormattedMessage {...loginMessages.exampleDemonstration} />
|
|
97
|
+
</Text>
|
|
98
|
+
<Text>
|
|
99
|
+
<FormattedMessage {...loginMessages.setupInstructions} />
|
|
100
|
+
</Text>
|
|
101
|
+
<Button variant="primary" onClick={authorize}>
|
|
102
|
+
{intl.formatMessage(loginMessages.signIntoCanva)}
|
|
103
|
+
</Button>
|
|
104
|
+
</Rows>
|
|
105
|
+
)}
|
|
106
|
+
</Box>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const loginMessages = defineMessages({
|
|
112
|
+
authorizationError: {
|
|
113
|
+
defaultMessage: "Authorization error",
|
|
114
|
+
description:
|
|
115
|
+
"Title displayed when there is an error during OAuth authorization",
|
|
116
|
+
},
|
|
117
|
+
tryAgain: {
|
|
118
|
+
defaultMessage: "Try again",
|
|
119
|
+
description: "Button text to retry authorization after an error occurs",
|
|
120
|
+
},
|
|
121
|
+
signInRequired: {
|
|
122
|
+
defaultMessage: "Sign in required",
|
|
123
|
+
description:
|
|
124
|
+
"Header title for the login page indicating authentication is needed",
|
|
125
|
+
},
|
|
126
|
+
dataConnectorsOAuth: {
|
|
127
|
+
defaultMessage:
|
|
128
|
+
"Data connectors can use OAuth to authenticate with other platforms.",
|
|
129
|
+
description: "Body text shown when the user is prompted to sign in",
|
|
130
|
+
},
|
|
131
|
+
exampleDemonstration: {
|
|
132
|
+
defaultMessage:
|
|
133
|
+
"This example demonstrates how to do this with the Canva Connect API.",
|
|
134
|
+
description: "Body text shown when the user is prompted to sign in",
|
|
135
|
+
},
|
|
136
|
+
setupInstructions: {
|
|
137
|
+
defaultMessage:
|
|
138
|
+
"For set up instructions please see the README.md in the root folder.",
|
|
139
|
+
description: "Body text shown when the user is prompted to sign in",
|
|
140
|
+
},
|
|
141
|
+
signIntoCanva: {
|
|
142
|
+
defaultMessage: "Sign into Canva",
|
|
143
|
+
description: "Button text for initiating Canva authentication",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Box, Rows } from "@canva/app-ui-kit";
|
|
2
|
+
import { useIntl } from "react-intl";
|
|
3
|
+
import { DATA_SOURCES } from "src/api/data_sources";
|
|
4
|
+
import { Footer, Header } from "src/components";
|
|
5
|
+
|
|
6
|
+
export const SelectSource = () => {
|
|
7
|
+
const intl = useIntl();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Box paddingEnd="2u" paddingTop="2u">
|
|
11
|
+
<Rows spacing="1u" align="start">
|
|
12
|
+
<Header
|
|
13
|
+
title={intl.formatMessage({
|
|
14
|
+
defaultMessage: "What would you like to import? ",
|
|
15
|
+
description: "The header text for the data source selection view",
|
|
16
|
+
})}
|
|
17
|
+
showBack={false}
|
|
18
|
+
/>
|
|
19
|
+
{DATA_SOURCES.map((handler) => handler.selectionPage())}
|
|
20
|
+
<Footer />
|
|
21
|
+
</Rows>
|
|
22
|
+
</Box>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { useAppContext } from "../context";
|
|
4
|
+
import { Paths } from "../routes";
|
|
5
|
+
|
|
6
|
+
interface ProtectedRouteProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A component that protects routes from unauthorized access.
|
|
12
|
+
* If the user is not authenticated, they will be redirected to the login page.
|
|
13
|
+
*/
|
|
14
|
+
export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
|
15
|
+
const { isAuthenticated } = useAppContext();
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!isAuthenticated) {
|
|
20
|
+
navigate(Paths.LOGIN);
|
|
21
|
+
}
|
|
22
|
+
}, [isAuthenticated, navigate]);
|
|
23
|
+
|
|
24
|
+
return <>{children}</>;
|
|
25
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Entrypoint } from "src/entrypoint";
|
|
2
|
+
import { Home } from "src/home";
|
|
3
|
+
import { DataSourceConfig, ErrorPage, Login, SelectSource } from "src/pages";
|
|
4
|
+
import { ProtectedRoute } from "./protected_route";
|
|
5
|
+
|
|
6
|
+
export enum Paths {
|
|
7
|
+
ENTRYPOINT = "/",
|
|
8
|
+
LOGIN = "/login",
|
|
9
|
+
DATA_SOURCE_SELECTION = "/data-source-selection",
|
|
10
|
+
DATA_SOURCE_CONFIG = "/data-source-config",
|
|
11
|
+
ERRORS = "/errors/:retry",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const routes = [
|
|
15
|
+
{
|
|
16
|
+
path: Paths.ENTRYPOINT,
|
|
17
|
+
element: <Home />,
|
|
18
|
+
errorElement: <ErrorPage />,
|
|
19
|
+
children: [
|
|
20
|
+
{
|
|
21
|
+
index: true,
|
|
22
|
+
element: <Entrypoint />,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
path: Paths.LOGIN,
|
|
26
|
+
element: <Login />,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: Paths.DATA_SOURCE_SELECTION,
|
|
30
|
+
element: (
|
|
31
|
+
<ProtectedRoute>
|
|
32
|
+
<SelectSource />
|
|
33
|
+
</ProtectedRoute>
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: Paths.DATA_SOURCE_CONFIG,
|
|
38
|
+
element: (
|
|
39
|
+
<ProtectedRoute>
|
|
40
|
+
<DataSourceConfig />
|
|
41
|
+
</ProtectedRoute>
|
|
42
|
+
),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RenderSelectionUiRequest } from "@canva/intents/data";
|
|
2
|
+
|
|
3
|
+
export const isLaunchedWithError = (request: RenderSelectionUiRequest) => {
|
|
4
|
+
return request.invocationContext.reason === "app_error";
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const isOutdatedSource = (request: RenderSelectionUiRequest) => {
|
|
8
|
+
return request.invocationContext.reason === "outdated_source_ref";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const isDataRefEmpty = (request: RenderSelectionUiRequest) => {
|
|
12
|
+
return (
|
|
13
|
+
!request?.invocationContext ||
|
|
14
|
+
(!isLaunchedWithError(request) &&
|
|
15
|
+
!request.invocationContext.dataSourceRef?.source)
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BooleanDataTableCell,
|
|
3
|
+
ColumnConfig,
|
|
4
|
+
DataTable,
|
|
5
|
+
DataTableCell,
|
|
6
|
+
DateDataTableCell,
|
|
7
|
+
NumberDataTableCell,
|
|
8
|
+
StringDataTableCell,
|
|
9
|
+
} from "@canva/intents/data";
|
|
10
|
+
import type { APIResponseItem } from "src/api";
|
|
11
|
+
|
|
12
|
+
export interface DataTableColumn<T extends APIResponseItem> {
|
|
13
|
+
label: string;
|
|
14
|
+
getValue: keyof T | ((result: T) => boolean | string | number | Date);
|
|
15
|
+
toCell: (value) => DataTableCell;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function toDataTable<T extends APIResponseItem>(
|
|
19
|
+
apiData: T[],
|
|
20
|
+
columns: DataTableColumn<T>[],
|
|
21
|
+
rowLimit: number,
|
|
22
|
+
): DataTable {
|
|
23
|
+
const items = apiData.slice(0, rowLimit);
|
|
24
|
+
const dataTable: DataTable = {
|
|
25
|
+
columnConfigs: columnConfig(columns),
|
|
26
|
+
rows: [],
|
|
27
|
+
};
|
|
28
|
+
items.forEach((item) => {
|
|
29
|
+
const cells = columns.map((column) => {
|
|
30
|
+
const value =
|
|
31
|
+
typeof column.getValue === "function"
|
|
32
|
+
? column.getValue(item)
|
|
33
|
+
: item[column.getValue];
|
|
34
|
+
return column.toCell(value);
|
|
35
|
+
});
|
|
36
|
+
dataTable.rows.push({ cells });
|
|
37
|
+
});
|
|
38
|
+
return dataTable;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Converts an array of DataTableColumn to ColumnConfig.
|
|
43
|
+
* @param columns Array of DataTableColumn
|
|
44
|
+
* @returns Array of ColumnConfig
|
|
45
|
+
*/
|
|
46
|
+
function columnConfig<T extends APIResponseItem>(
|
|
47
|
+
columns: DataTableColumn<T>[],
|
|
48
|
+
): ColumnConfig[] {
|
|
49
|
+
return columns.map((column) => ({
|
|
50
|
+
name: column.label,
|
|
51
|
+
type: column.toCell({} as unknown).type, // Use an empty object to infer the type
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates a string cell for the data table.
|
|
57
|
+
* @param value String containing up to 10,000 characters
|
|
58
|
+
*/
|
|
59
|
+
export function stringCell(value: string): StringDataTableCell {
|
|
60
|
+
return {
|
|
61
|
+
type: "string",
|
|
62
|
+
value,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates a number cell for the data table.
|
|
68
|
+
* @param value Number within range `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER`
|
|
69
|
+
* @param formatting Formatting using ISO/IEC 29500-1:2016 Office Open XML Format
|
|
70
|
+
*/
|
|
71
|
+
export function numberCell(
|
|
72
|
+
value: number,
|
|
73
|
+
formatting?: string,
|
|
74
|
+
): NumberDataTableCell {
|
|
75
|
+
return {
|
|
76
|
+
type: "number",
|
|
77
|
+
value,
|
|
78
|
+
metadata: {
|
|
79
|
+
formatting,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Creates a boolean cell for the data table.
|
|
86
|
+
* @param value Boolean value
|
|
87
|
+
*/
|
|
88
|
+
export function booleanCell(value: boolean): BooleanDataTableCell {
|
|
89
|
+
return {
|
|
90
|
+
type: "boolean",
|
|
91
|
+
value,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Creates a date cell for the data table.
|
|
97
|
+
* @param value Number, Date or String
|
|
98
|
+
* @description If value is a string, it will be parsed as a date. If value is a number, it will be treated as a timestamp in seconds.
|
|
99
|
+
*/
|
|
100
|
+
export function dateCell(value: number | Date | string): DateDataTableCell {
|
|
101
|
+
// if string, parse as date
|
|
102
|
+
if (typeof value === "string") {
|
|
103
|
+
value = new Date(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// if is date, convert to timestamp in seconds
|
|
107
|
+
if (value instanceof Date) {
|
|
108
|
+
value = value.valueOf() / 1000;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
type: "date",
|
|
113
|
+
value,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataTable,
|
|
3
|
+
DataTableMetadata,
|
|
4
|
+
GetDataTableCompleted,
|
|
5
|
+
GetDataTableError,
|
|
6
|
+
} from "@canva/intents/data";
|
|
7
|
+
|
|
8
|
+
export const completeDataTable = (
|
|
9
|
+
dataTable: DataTable,
|
|
10
|
+
metadata?: DataTableMetadata,
|
|
11
|
+
): GetDataTableCompleted => {
|
|
12
|
+
return {
|
|
13
|
+
status: "completed",
|
|
14
|
+
dataTable,
|
|
15
|
+
metadata,
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const appError = (message?: string): GetDataTableError => {
|
|
20
|
+
return {
|
|
21
|
+
status: "app_error",
|
|
22
|
+
message,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const outdatedSourceRef = (): GetDataTableError => {
|
|
27
|
+
return {
|
|
28
|
+
status: "outdated_source_ref",
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const remoteRequestFailed = (): GetDataTableError => {
|
|
33
|
+
return {
|
|
34
|
+
status: "remote_request_failed",
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/* eslint-disable formatjs/no-literal-string-in-object */
|
|
2
|
+
import type { APIResponseItem } from "src/api";
|
|
3
|
+
import {
|
|
4
|
+
booleanCell,
|
|
5
|
+
dateCell,
|
|
6
|
+
numberCell,
|
|
7
|
+
stringCell,
|
|
8
|
+
toDataTable,
|
|
9
|
+
} from "../data_table";
|
|
10
|
+
import type { DataTableColumn } from "../data_table";
|
|
11
|
+
|
|
12
|
+
describe("data table utils", () => {
|
|
13
|
+
interface TestItem extends APIResponseItem {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
count: number;
|
|
17
|
+
active: boolean;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const testItems: TestItem[] = [
|
|
22
|
+
{
|
|
23
|
+
id: "item1",
|
|
24
|
+
name: "Test Item 1",
|
|
25
|
+
count: 42,
|
|
26
|
+
active: true,
|
|
27
|
+
createdAt: "2023-01-01T00:00:00Z",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "item2",
|
|
31
|
+
name: "Test Item 2",
|
|
32
|
+
count: 0,
|
|
33
|
+
active: false,
|
|
34
|
+
createdAt: "2023-02-15T12:30:45Z",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const columns: DataTableColumn<TestItem>[] = [
|
|
39
|
+
{
|
|
40
|
+
label: "ID",
|
|
41
|
+
getValue: "id",
|
|
42
|
+
toCell: stringCell,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: "Name",
|
|
46
|
+
getValue: "name",
|
|
47
|
+
toCell: stringCell,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: "Count",
|
|
51
|
+
getValue: "count",
|
|
52
|
+
toCell: numberCell,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
label: "Status",
|
|
56
|
+
getValue: "active",
|
|
57
|
+
toCell: booleanCell,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
label: "Created",
|
|
61
|
+
getValue: "createdAt",
|
|
62
|
+
toCell: dateCell,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: "Custom",
|
|
66
|
+
getValue: (item) => `${item.name} (${item.count})`,
|
|
67
|
+
toCell: stringCell,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
test("toDataTable creates correct structure", () => {
|
|
72
|
+
const result = toDataTable(testItems, columns, 10);
|
|
73
|
+
|
|
74
|
+
expect(result.rows.length).toBe(testItems.length);
|
|
75
|
+
expect(result.columnConfigs?.map((cell) => cell.name)).toEqual([
|
|
76
|
+
"ID",
|
|
77
|
+
"Name",
|
|
78
|
+
"Count",
|
|
79
|
+
"Status",
|
|
80
|
+
"Created",
|
|
81
|
+
"Custom",
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("toDataTable respects row limit", () => {
|
|
86
|
+
const result = toDataTable(testItems, columns, 1);
|
|
87
|
+
|
|
88
|
+
expect(result.rows.length).toBe(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("toDataTable handles function-based getValue", () => {
|
|
92
|
+
const result = toDataTable(testItems, columns, 10);
|
|
93
|
+
|
|
94
|
+
// Check the custom column values
|
|
95
|
+
expect(result.rows[0].cells[5].value).toBe("Test Item 1 (42)");
|
|
96
|
+
expect(result.rows[1].cells[5].value).toBe("Test Item 2 (0)");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("cell formatter functions create correct cell structures", () => {
|
|
100
|
+
expect(stringCell("test")).toEqual({ type: "string", value: "test" });
|
|
101
|
+
|
|
102
|
+
expect(numberCell(42)).toEqual({
|
|
103
|
+
type: "number",
|
|
104
|
+
value: 42,
|
|
105
|
+
metadata: { formatting: undefined },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(numberCell(42, "#,##0")).toEqual({
|
|
109
|
+
type: "number",
|
|
110
|
+
value: 42,
|
|
111
|
+
metadata: { formatting: "#,##0" },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(booleanCell(true)).toEqual({ type: "boolean", value: true });
|
|
115
|
+
|
|
116
|
+
// Test date with string input
|
|
117
|
+
const dateResult1 = dateCell("2023-01-01T00:00:00Z");
|
|
118
|
+
expect(dateResult1.type).toBe("date");
|
|
119
|
+
expect(typeof dateResult1.value).toBe("number");
|
|
120
|
+
|
|
121
|
+
// Test date with Date object input
|
|
122
|
+
const dateObj = new Date("2023-01-01T00:00:00Z");
|
|
123
|
+
const dateResult2 = dateCell(dateObj);
|
|
124
|
+
expect(dateResult2.type).toBe("date");
|
|
125
|
+
expect(typeof dateResult2.value).toBe("number");
|
|
126
|
+
|
|
127
|
+
// Test date with timestamp input
|
|
128
|
+
const timestamp = dateObj.valueOf() / 1000; // seconds
|
|
129
|
+
const dateResult3 = dateCell(timestamp);
|
|
130
|
+
expect(dateResult3.type).toBe("date");
|
|
131
|
+
expect(dateResult3.value).toBe(timestamp);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* Scroll container */
|
|
2
|
+
.scrollContainer {
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
overflow-y: scroll;
|
|
5
|
+
height: 100%;
|
|
6
|
+
padding-top: var(--ui-kit-space-2);
|
|
7
|
+
padding-right: var(--ui-kit-space-2);
|
|
8
|
+
padding-bottom: var(--ui-kit-space-2);
|
|
9
|
+
|
|
10
|
+
/* for firefox */
|
|
11
|
+
scrollbar-width: thin;
|
|
12
|
+
scrollbar-color: var(--ui-kit-color-typography-quaternary) transparent;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.scrollContainer::-webkit-scrollbar {
|
|
16
|
+
position: absolute;
|
|
17
|
+
width: var(--ui-kit-base-unit);
|
|
18
|
+
height: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.scrollContainer::-webkit-scrollbar-track {
|
|
22
|
+
background: transparent;
|
|
23
|
+
width: var(--ui-kit-base-unit);
|
|
24
|
+
margin-top: var(--ui-kit-space-1);
|
|
25
|
+
margin-bottom: var(--ui-kit-space-1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.scrollContainer::-webkit-scrollbar-thumb {
|
|
29
|
+
border-radius: var(--ui-kit-border-radius);
|
|
30
|
+
background: var(--ui-kit-color-typography-quaternary);
|
|
31
|
+
visibility: hidden;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.scrollContainer:hover::-webkit-scrollbar-thumb,
|
|
35
|
+
.scrollContainer:focus::-webkit-scrollbar-thumb,
|
|
36
|
+
.scrollContainer:focus-within::-webkit-scrollbar-thumb {
|
|
37
|
+
visibility: visible;
|
|
38
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"jsx": "react-jsx",
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"es2018",
|
|
8
|
+
"es2019.array",
|
|
9
|
+
"es2019.object",
|
|
10
|
+
"es2019.string",
|
|
11
|
+
"es2020.promise",
|
|
12
|
+
"es2020.string"
|
|
13
|
+
],
|
|
14
|
+
"types": ["node", "webpack-env", "jest"],
|
|
15
|
+
"composite": false,
|
|
16
|
+
"declaration": false,
|
|
17
|
+
"declarationMap": false,
|
|
18
|
+
"experimentalDecorators": true,
|
|
19
|
+
"importHelpers": true,
|
|
20
|
+
"noImplicitOverride": true,
|
|
21
|
+
"moduleResolution": "node",
|
|
22
|
+
"rootDir": ".",
|
|
23
|
+
"outDir": "dist",
|
|
24
|
+
"strict": true,
|
|
25
|
+
"skipLibCheck": true,
|
|
26
|
+
"target": "ES2019",
|
|
27
|
+
"sourceMap": true,
|
|
28
|
+
"inlineSources": true,
|
|
29
|
+
"module": "ESNext",
|
|
30
|
+
"noImplicitAny": false,
|
|
31
|
+
"removeComments": true,
|
|
32
|
+
"preserveConstEnums": true,
|
|
33
|
+
"allowSyntheticDefaultImports": true,
|
|
34
|
+
"baseUrl": "./",
|
|
35
|
+
"paths": {
|
|
36
|
+
"assets": ["./assets"],
|
|
37
|
+
"styles": ["./styles"]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"include": [
|
|
41
|
+
"./src/**/*",
|
|
42
|
+
"./backend/**/*",
|
|
43
|
+
"./utils/**/*",
|
|
44
|
+
"./scripts/**/*",
|
|
45
|
+
"./declarations/declarations.d.ts",
|
|
46
|
+
"./styles/**/*",
|
|
47
|
+
"./node_modules/@types/**/*"
|
|
48
|
+
],
|
|
49
|
+
"ts-node": {
|
|
50
|
+
"compilerOptions": {
|
|
51
|
+
"module": "commonjs"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|