@canva/cli 0.0.1-beta.8 → 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 +852 -427
- 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 +29 -26
- 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/{dam/webpack.config.cjs → base/webpack.config.ts} +51 -67
- package/templates/common/.env.template +3 -3
- package/templates/common/README.md +16 -17
- package/templates/common/jest.config.mjs +29 -2
- package/templates/common/jest.setup.ts +20 -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 +43 -40
- 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} +51 -67
- 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/{gen_ai/webpack.config.cjs → data_connector/webpack.config.ts} +51 -67
- 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 +47 -42
- 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 +16 -10
- 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 +43 -0
- package/templates/gen_ai/src/home.tsx +13 -0
- package/templates/gen_ai/src/index.tsx +2 -18
- package/templates/gen_ai/src/pages/error.tsx +2 -2
- package/templates/gen_ai/src/pages/results.tsx +1 -1
- package/templates/gen_ai/src/routes/routes.tsx +2 -2
- 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} +51 -67
- package/templates/hello_world/canva-app.json +16 -0
- package/templates/hello_world/eslint.config.mjs +2 -25
- package/templates/hello_world/package.json +42 -37
- 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 +21 -1
- package/templates/hello_world/src/index.tsx +1 -1
- package/templates/hello_world/src/tests/__snapshots__/app.tests.tsx.snap +45 -0
- package/templates/hello_world/src/tests/app.tests.tsx +86 -0
- 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 -38
- /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,99 @@
|
|
|
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
|
+
});
|
|
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
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { RenderSelectionUiRequest } from "@canva/intents/data";
|
|
2
|
+
import type { AccessTokenResponse } from "@canva/user";
|
|
3
|
+
import { auth } from "@canva/user";
|
|
4
|
+
import { createContext, useCallback, useState } from "react";
|
|
5
|
+
import type { APIResponseItem, DataSourceHandler } from "src/api/data_source";
|
|
6
|
+
import { type DataSourceConfig } from "src/api/data_source";
|
|
7
|
+
|
|
8
|
+
export interface AppContextType {
|
|
9
|
+
appError: string;
|
|
10
|
+
setAppError: (value: string) => void;
|
|
11
|
+
request: RenderSelectionUiRequest;
|
|
12
|
+
isAuthenticated: boolean;
|
|
13
|
+
accessToken: AccessTokenResponse | undefined;
|
|
14
|
+
setAccessToken: (token: AccessTokenResponse | undefined) => void;
|
|
15
|
+
oauth: ReturnType<typeof auth.initOauth>;
|
|
16
|
+
logout: () => Promise<void>;
|
|
17
|
+
dataSourceHandler?: DataSourceHandler<DataSourceConfig, APIResponseItem>;
|
|
18
|
+
setDataSourceHandler: (
|
|
19
|
+
value: DataSourceHandler<DataSourceConfig, APIResponseItem>,
|
|
20
|
+
) => void;
|
|
21
|
+
|
|
22
|
+
dataSourceConfig?: DataSourceConfig;
|
|
23
|
+
setDataSourceConfig: (value: DataSourceConfig) => void;
|
|
24
|
+
loadDataSource: (title: string, source: DataSourceConfig) => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const AppContext = createContext<AppContextType>({
|
|
28
|
+
appError: "",
|
|
29
|
+
setAppError: () => {},
|
|
30
|
+
request: {} as RenderSelectionUiRequest,
|
|
31
|
+
isAuthenticated: false,
|
|
32
|
+
accessToken: undefined,
|
|
33
|
+
setAccessToken: () => {},
|
|
34
|
+
oauth: auth.initOauth(),
|
|
35
|
+
logout: async () => {},
|
|
36
|
+
dataSourceHandler: {} as DataSourceHandler<DataSourceConfig, APIResponseItem>,
|
|
37
|
+
setDataSourceHandler: () => {},
|
|
38
|
+
dataSourceConfig: {} as DataSourceConfig,
|
|
39
|
+
setDataSourceConfig: () => {},
|
|
40
|
+
loadDataSource: async () => {},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Provides application-wide state and methods using React Context.
|
|
45
|
+
* @param {object} props - The props object.
|
|
46
|
+
* @param {React.ReactNode} props.children - The children components wrapped by the provider.
|
|
47
|
+
* @returns {JSX.Element} The provider component.
|
|
48
|
+
* @description This provider component wraps the entire application to provide application-wide state and methods using React Context.
|
|
49
|
+
* It manages state related to app errors, filter parameters, and authentication.
|
|
50
|
+
* It exposes these state values and setter methods to its child components via the AppContext.
|
|
51
|
+
* For more information on React Context, refer to the official React documentation: {@link https://react.dev/learn/passing-data-deeply-with-context}.
|
|
52
|
+
*/
|
|
53
|
+
export const ContextProvider = ({
|
|
54
|
+
renderSelectionUiRequest,
|
|
55
|
+
children,
|
|
56
|
+
}: {
|
|
57
|
+
renderSelectionUiRequest: RenderSelectionUiRequest;
|
|
58
|
+
children: React.ReactNode;
|
|
59
|
+
}): JSX.Element => {
|
|
60
|
+
const [appError, setAppError] = useState<string>("");
|
|
61
|
+
const [request] = useState<RenderSelectionUiRequest>(
|
|
62
|
+
renderSelectionUiRequest,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// authentication
|
|
66
|
+
const [accessToken, setAccessToken] = useState<
|
|
67
|
+
AccessTokenResponse | undefined
|
|
68
|
+
>(undefined);
|
|
69
|
+
const oauth = auth.initOauth();
|
|
70
|
+
const isAuthenticated = !!accessToken;
|
|
71
|
+
|
|
72
|
+
// data handlers
|
|
73
|
+
const [dataSourceHandler, setDataSourceHandler] =
|
|
74
|
+
useState<DataSourceHandler<DataSourceConfig, APIResponseItem>>();
|
|
75
|
+
const [dataSourceConfig, setDataSourceConfig] = useState<DataSourceConfig>();
|
|
76
|
+
|
|
77
|
+
// data connection
|
|
78
|
+
const loadDataSource = useCallback(
|
|
79
|
+
async (title: string, source: DataSourceConfig) => {
|
|
80
|
+
const result = await request.updateDataRef({
|
|
81
|
+
title,
|
|
82
|
+
source: JSON.stringify(source),
|
|
83
|
+
});
|
|
84
|
+
if (result.status === "remote_request_failed") {
|
|
85
|
+
setAppError(`Failed to load data source: uanble to connect to the API`);
|
|
86
|
+
} else if (result.status === "app_error") {
|
|
87
|
+
setAppError(
|
|
88
|
+
`Failed to load data source: ${result.message || result.status}`,
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
setAppError("");
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
[request],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const logout = useCallback(async () => {
|
|
98
|
+
try {
|
|
99
|
+
setAccessToken(undefined);
|
|
100
|
+
await oauth.deauthorize();
|
|
101
|
+
setAccessToken(null);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
setAppError(error instanceof Error ? error.message : "Logout failed");
|
|
104
|
+
}
|
|
105
|
+
}, [oauth]);
|
|
106
|
+
|
|
107
|
+
const value: AppContextType = {
|
|
108
|
+
appError,
|
|
109
|
+
setAppError,
|
|
110
|
+
request,
|
|
111
|
+
isAuthenticated,
|
|
112
|
+
accessToken,
|
|
113
|
+
setAccessToken,
|
|
114
|
+
oauth,
|
|
115
|
+
logout,
|
|
116
|
+
dataSourceHandler,
|
|
117
|
+
setDataSourceHandler,
|
|
118
|
+
dataSourceConfig,
|
|
119
|
+
setDataSourceConfig,
|
|
120
|
+
loadDataSource,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
|
124
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import type { AppContextType } from ".";
|
|
3
|
+
import { AppContext } from ".";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A custom React hook to access the application-wide state and methods provided by the ContextProvider using React Context.
|
|
7
|
+
* @returns {AppContextType} - An object containing application-wide state and methods.
|
|
8
|
+
* @throws {Error} - Throws an error if used outside the context of a ContextProvider.
|
|
9
|
+
* @description This hook allows components to access the application-wide state and methods provided by the ContextProvider using React Context. It retrieves the context value using the useContext hook and ensures that the context is available. If used outside the context of an ContextProvider, it throws an error instructing developers to use it within an ContextProvider.
|
|
10
|
+
*/
|
|
11
|
+
export const useAppContext = (): AppContextType => {
|
|
12
|
+
const context = useContext(AppContext);
|
|
13
|
+
if (!context) {
|
|
14
|
+
throw new Error("useAppContext must be used within a ContextProvider");
|
|
15
|
+
}
|
|
16
|
+
return context;
|
|
17
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { LoadingIndicator } from "@canva/app-ui-kit";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { useNavigate } from "react-router-dom";
|
|
4
|
+
import type {
|
|
5
|
+
APIResponseItem,
|
|
6
|
+
DataSourceConfig,
|
|
7
|
+
DataSourceHandler,
|
|
8
|
+
} from "./api";
|
|
9
|
+
import { DATA_SOURCES } from "./api/data_sources";
|
|
10
|
+
import { useAppContext } from "./context";
|
|
11
|
+
import { Paths } from "./routes";
|
|
12
|
+
import {
|
|
13
|
+
isDataRefEmpty,
|
|
14
|
+
isLaunchedWithError,
|
|
15
|
+
isOutdatedSource,
|
|
16
|
+
} from "./utils/data_params";
|
|
17
|
+
|
|
18
|
+
const parseDataSource = (source: string) => {
|
|
19
|
+
try {
|
|
20
|
+
return source ? JSON.parse(source) : undefined;
|
|
21
|
+
} catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Entrypoint = () => {
|
|
27
|
+
const navigate = useNavigate();
|
|
28
|
+
const context = useAppContext();
|
|
29
|
+
const { request, setAppError, setDataSourceHandler } = context;
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// if the app was loaded with a populated data ref, we should reload the previous state.
|
|
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
|
+
let navigateTo: Paths | undefined;
|
|
35
|
+
|
|
36
|
+
if (isDataRefEmpty(request)) {
|
|
37
|
+
// probably a first time launch - the user will need to select a data source
|
|
38
|
+
navigateTo = Paths.DATA_SOURCE_SELECTION;
|
|
39
|
+
} else if (isOutdatedSource(request) || isLaunchedWithError(request)) {
|
|
40
|
+
// the configured source does not match the expected data source types
|
|
41
|
+
// so prompt the user to reconfigure the data source
|
|
42
|
+
setAppError("The data source configuration needs to be updated.");
|
|
43
|
+
navigateTo = Paths.DATA_SOURCE_SELECTION;
|
|
44
|
+
} else {
|
|
45
|
+
// there is a data ref, so we should parse it and navigate to the appropriate page
|
|
46
|
+
const dataRef = request.invocationContext.dataSourceRef;
|
|
47
|
+
const parsedSource = parseDataSource(dataRef?.source ?? "");
|
|
48
|
+
|
|
49
|
+
if (parsedSource) {
|
|
50
|
+
const dataHandler = DATA_SOURCES.find((handler) =>
|
|
51
|
+
handler.matchSource(parsedSource),
|
|
52
|
+
);
|
|
53
|
+
if (dataHandler) {
|
|
54
|
+
setDataSourceHandler(
|
|
55
|
+
dataHandler as unknown as DataSourceHandler<
|
|
56
|
+
DataSourceConfig,
|
|
57
|
+
APIResponseItem
|
|
58
|
+
>,
|
|
59
|
+
);
|
|
60
|
+
dataHandler.sourceConfig = parsedSource;
|
|
61
|
+
navigateTo = Paths.DATA_SOURCE_CONFIG;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
navigate(navigateTo || Paths.DATA_SOURCE_SELECTION);
|
|
67
|
+
}, [request]);
|
|
68
|
+
|
|
69
|
+
return <LoadingIndicator />;
|
|
70
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Box, Rows } from "@canva/app-ui-kit";
|
|
2
|
+
import { Outlet } from "react-router-dom";
|
|
3
|
+
import * as styles from "styles/components.css";
|
|
4
|
+
import { AppError } from "./components";
|
|
5
|
+
|
|
6
|
+
export const Home = () => (
|
|
7
|
+
<div className={styles.scrollContainer}>
|
|
8
|
+
<Box
|
|
9
|
+
justifyContent="center"
|
|
10
|
+
width="full"
|
|
11
|
+
alignItems="start"
|
|
12
|
+
display="flex"
|
|
13
|
+
height="full"
|
|
14
|
+
>
|
|
15
|
+
<Rows spacing="3u">
|
|
16
|
+
<AppError />
|
|
17
|
+
<Outlet />
|
|
18
|
+
</Rows>
|
|
19
|
+
</Box>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Alert, AppUiProvider } from "@canva/app-ui-kit";
|
|
2
|
+
import type {
|
|
3
|
+
GetDataTableRequest,
|
|
4
|
+
GetDataTableResponse,
|
|
5
|
+
RenderSelectionUiRequest,
|
|
6
|
+
} from "@canva/intents/data";
|
|
7
|
+
import { prepareDataConnector } from "@canva/intents/data";
|
|
8
|
+
import { auth } from "@canva/user";
|
|
9
|
+
import { createRoot } from "react-dom/client";
|
|
10
|
+
import { buildDataTableResult, scope } from "./api";
|
|
11
|
+
import { App } from "./app";
|
|
12
|
+
import "@canva/app-ui-kit/styles.css";
|
|
13
|
+
|
|
14
|
+
const root = createRoot(document.getElementById("root") as Element);
|
|
15
|
+
prepareDataConnector({
|
|
16
|
+
/**
|
|
17
|
+
* Fetches structured data from an external source.
|
|
18
|
+
*
|
|
19
|
+
* This action is called in two scenarios:
|
|
20
|
+
*
|
|
21
|
+
* - During data selection to preview data before import (when {@link RenderSelectionUiRequest.updateDataRef} is called).
|
|
22
|
+
* - When refreshing previously imported data (when the user requests an update).
|
|
23
|
+
*
|
|
24
|
+
* @param params - Parameters for the data fetching operation.
|
|
25
|
+
* @returns A promise resolving to either a successful result with data or an error.
|
|
26
|
+
*/
|
|
27
|
+
getDataTable: async (
|
|
28
|
+
params: GetDataTableRequest,
|
|
29
|
+
): Promise<GetDataTableResponse> => {
|
|
30
|
+
const oauth = auth.initOauth();
|
|
31
|
+
const token = await oauth.getAccessToken({ scope });
|
|
32
|
+
return buildDataTableResult(params, token?.token);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Renders a UI component for selecting and configuring data from external sources.
|
|
37
|
+
* This UI should allow users to browse data sources, apply filters, and select data.
|
|
38
|
+
* When selection is complete, the implementation must call the `updateDataRef`
|
|
39
|
+
* callback provided in the params to preview and confirm the data selection.
|
|
40
|
+
*
|
|
41
|
+
* @param request - parameters that provide context and configuration for the data selection UI.
|
|
42
|
+
* Contains invocation context, size limits, and the updateDataRef callback
|
|
43
|
+
*/
|
|
44
|
+
renderSelectionUi: async (request: RenderSelectionUiRequest) => {
|
|
45
|
+
function render() {
|
|
46
|
+
root.render(<App request={request} />);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
render();
|
|
50
|
+
|
|
51
|
+
if (module.hot) {
|
|
52
|
+
module.hot.accept("./app", render);
|
|
53
|
+
module.hot.accept("./api", render);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// TODO: Fallback message if you have not turned on the data connector intent.
|
|
59
|
+
// You can remove this once your app is correctly configured.
|
|
60
|
+
root.render(
|
|
61
|
+
<AppUiProvider>
|
|
62
|
+
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
63
|
+
<Alert tone="critical">
|
|
64
|
+
If you're seeing this, you need to turn on the data connector intent in
|
|
65
|
+
the Developer Portal for this app.
|
|
66
|
+
</Alert>
|
|
67
|
+
</AppUiProvider>,
|
|
68
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useAppContext } from "src/context";
|
|
2
|
+
|
|
3
|
+
export const DataSourceConfig = () => {
|
|
4
|
+
const { dataSourceHandler } = useAppContext();
|
|
5
|
+
if (!dataSourceHandler) {
|
|
6
|
+
return undefined; // should be impossible
|
|
7
|
+
}
|
|
8
|
+
return dataSourceHandler.configPage(dataSourceHandler.sourceConfig);
|
|
9
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Button, Rows, Text } from "@canva/app-ui-kit";
|
|
2
|
+
import { FormattedMessage, useIntl } from "react-intl";
|
|
3
|
+
import { useNavigate } from "react-router-dom";
|
|
4
|
+
import { Paths } from "src/routes";
|
|
5
|
+
import * as styles from "styles/components.css";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Bare bones Error Page, please add relevant information and behavior that your app requires.
|
|
9
|
+
*/
|
|
10
|
+
export const ErrorPage = () => {
|
|
11
|
+
const navigate = useNavigate();
|
|
12
|
+
const intl = useIntl();
|
|
13
|
+
|
|
14
|
+
const onClick = () => {
|
|
15
|
+
navigate(Paths.ENTRYPOINT);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className={styles.scrollContainer}>
|
|
20
|
+
<Rows spacing="2u">
|
|
21
|
+
<Text>
|
|
22
|
+
<FormattedMessage
|
|
23
|
+
defaultMessage="Something went wrong."
|
|
24
|
+
description="A message to indicate that something went wrong, but no more information is available"
|
|
25
|
+
/>
|
|
26
|
+
</Text>
|
|
27
|
+
<Button variant="primary" onClick={onClick} stretch={true}>
|
|
28
|
+
{intl.formatMessage({
|
|
29
|
+
defaultMessage: "Start over",
|
|
30
|
+
description:
|
|
31
|
+
"A button label to clear the error and the prompt and start again",
|
|
32
|
+
})}
|
|
33
|
+
</Button>
|
|
34
|
+
</Rows>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
};
|