@aws/nx-plugin-mcp 0.120.0 → 0.121.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/bin/aws-nx-mcp.js +23 -35
- package/docs/guides/astro-docs.mdx +142 -0
- package/docs/guides/connection/py-agent-a2a.mdx +105 -0
- package/docs/guides/connection/py-agent-mcp.mdx +147 -0
- package/docs/guides/connection/react-agui.mdx +240 -0
- package/docs/guides/connection/react-fastapi.mdx +1476 -0
- package/docs/guides/connection/react-py-agent.mdx +198 -0
- package/docs/guides/connection/react-smithy.mdx +1355 -0
- package/docs/guides/connection/react-trpc.mdx +447 -0
- package/docs/guides/connection/react-ts-agent.mdx +198 -0
- package/docs/guides/connection/smithy-rdb.mdx +161 -0
- package/docs/guides/connection/trpc-rdb.mdx +127 -0
- package/docs/guides/connection/ts-agent-a2a.mdx +106 -0
- package/docs/guides/connection/ts-agent-mcp.mdx +144 -0
- package/docs/guides/connection/ts-agent-rdb.mdx +141 -0
- package/docs/guides/connection/ts-mcp-server-rdb.mdx +135 -0
- package/docs/guides/connection.mdx +126 -0
- package/docs/guides/docker-bundling.mdx +403 -0
- package/docs/guides/fastapi.mdx +509 -0
- package/docs/guides/license.mdx +293 -0
- package/docs/guides/nx-generator.mdx +588 -0
- package/docs/guides/py-agent.mdx +483 -0
- package/docs/guides/py-mcp-server.mdx +161 -0
- package/docs/guides/python-lambda-function.mdx +207 -0
- package/docs/guides/python-project.mdx +228 -0
- package/docs/guides/react-website-auth.mdx +216 -0
- package/docs/guides/react-website.mdx +421 -0
- package/docs/guides/runtime-config.mdx +312 -0
- package/docs/guides/terraform-project.mdx +317 -0
- package/docs/guides/trpc.mdx +816 -0
- package/docs/guides/ts-agent.mdx +436 -0
- package/docs/guides/ts-lambda-function.mdx +218 -0
- package/docs/guides/ts-mcp-server.mdx +169 -0
- package/docs/guides/ts-nx-plugin.mdx +159 -0
- package/docs/guides/ts-rdb.mdx +759 -0
- package/docs/guides/ts-smithy-api.mdx +661 -0
- package/docs/guides/typescript-infrastructure.mdx +408 -0
- package/docs/guides/typescript-project.mdx +312 -0
- package/docs/guides/workspace.mdx +181 -0
- package/docs/snippets/agent/architecture.mdx +72 -0
- package/docs/snippets/agent/bedrock-deployment.mdx +172 -0
- package/docs/snippets/agent/runtime-arn.mdx +64 -0
- package/docs/snippets/api/api-architecture.mdx +93 -0
- package/docs/snippets/api/api-choice-note.mdx +6 -0
- package/docs/snippets/api/cors-configuration-cdk-note.mdx +25 -0
- package/docs/snippets/api/cors-configuration-terraform-note.mdx +28 -0
- package/docs/snippets/api/shared-constructs.mdx +38 -0
- package/docs/snippets/api/type-safe-api-integrations.mdx +643 -0
- package/docs/snippets/api/waf-configuration.mdx +37 -0
- package/docs/snippets/connection/a2a-infrastructure.mdx +63 -0
- package/docs/snippets/connection/lambda-rdb-ssl-requirements.mdx +40 -0
- package/docs/snippets/connection/mcp-server-rdb-ssl-requirements.mdx +35 -0
- package/docs/snippets/connection/rdb-api-infrastructure.mdx +72 -0
- package/docs/snippets/connection/react-agent-infrastructure.mdx +61 -0
- package/docs/snippets/connection/strands-agent-rdb-ssl-requirements.mdx +35 -0
- package/docs/snippets/lambda-function/architecture.mdx +36 -0
- package/docs/snippets/lambda-function/deploying-your-function.mdx +118 -0
- package/docs/snippets/mcp/architecture.mdx +58 -0
- package/docs/snippets/mcp/assistant-docs.mdx +10 -0
- package/docs/snippets/mcp/bedrock-deployment.mdx +167 -0
- package/docs/snippets/mcp/config.mdx +13 -0
- package/docs/snippets/mcp/configuration-py.mdx +42 -0
- package/docs/snippets/mcp/configuration-ts.mdx +53 -0
- package/docs/snippets/mcp/observability.mdx +8 -0
- package/docs/snippets/mcp/shared-constructs.mdx +32 -0
- package/docs/snippets/pdk-migration/example/01-migrate-api.mdx +602 -0
- package/docs/snippets/pdk-migration/example/02-migrate-website.mdx +915 -0
- package/docs/snippets/pdk-migration/example/03-migrate-infra.mdx +161 -0
- package/docs/snippets/pdk-migration/example/04-deploy.mdx +229 -0
- package/docs/snippets/pdk-migration/faq/aws-arch.mdx +17 -0
- package/docs/snippets/pdk-migration/faq/cdk-graph.mdx +29 -0
- package/docs/snippets/pdk-migration/faq/infrastructure-python-java.mdx +19 -0
- package/docs/snippets/pdk-migration/faq/pdk-nag.mdx +15 -0
- package/docs/snippets/pdk-migration/faq/pipeline.mdx +15 -0
- package/docs/snippets/pdk-migration/faq/type-safe-api.mdx +310 -0
- package/docs/snippets/pdk-migration/faq/use-of-projen.mdx +15 -0
- package/docs/snippets/prerequisites.mdx +20 -0
- package/docs/snippets/required-prerequisites.mdx +12 -0
- package/docs/snippets/shared-constructs.mdx +40 -0
- package/docs/snippets/tools/acurl.mdx +73 -0
- package/docs/snippets/ts-bundle.mdx +14 -0
- package/package.json +1 -1
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Migrate the Website
|
|
3
|
+
---
|
|
4
|
+
import { Steps, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
5
|
+
import CreateNxWorkspaceCommand from '@components/create-nx-workspace-command.astro';
|
|
6
|
+
import RunGenerator from '@components/run-generator.astro';
|
|
7
|
+
import Link from '@components/link.astro';
|
|
8
|
+
import NxCommands from '@components/nx-commands.astro';
|
|
9
|
+
import Drawer from '@components/drawer.astro';
|
|
10
|
+
import InstallCommand from '@components/install-command.astro';
|
|
11
|
+
|
|
12
|
+
The `CloudscapeReactTsWebsiteProject` used in the shopping list application configured a React website with CloudScape and Cognito authentication built in.
|
|
13
|
+
|
|
14
|
+
This project type leveraged [`create-react-app`](https://github.com/facebook/create-react-app), which is now deprecated. For migrating the website in this guide, we will use the <Link path="/guides/react-website">`ts#react-website` generator</Link>, which uses more modern and supported technologies, namely [Vite](https://vite.dev/).
|
|
15
|
+
|
|
16
|
+
As part of the migration, we will also move from PDK's configured React Router to [TanStack Router](https://tanstack.com/router), which adds additional type-safety to website routing.
|
|
17
|
+
|
|
18
|
+
#### Generate a React Website
|
|
19
|
+
|
|
20
|
+
Run the <Link path="/guides/react-website">`ts#react-website` generator</Link> to set up your website project in `packages/website`:
|
|
21
|
+
|
|
22
|
+
<RunGenerator generator="ts#react-website" noInteractive requiredParameters={{ name: 'website' }} />
|
|
23
|
+
|
|
24
|
+
#### Add Cognito Authentication
|
|
25
|
+
|
|
26
|
+
The React website generator above doesn't bundle cognito authentication by default like `CloudscapeReactTsWebsiteProject`, instead it's added explicitly via the <Link path="/guides/react-website-auth">`ts#react-website#auth` generator</Link>.
|
|
27
|
+
|
|
28
|
+
<RunGenerator generator="ts#react-website#auth" noInteractive requiredParameters={{ project: 'website', cognitoDomain: 'shopping-list' }} />
|
|
29
|
+
|
|
30
|
+
This adds React components which manage the appropriate redirects to ensure users log in using the Cognito hosted UI. This also adds a CDK construct to deploy the Cognito resources in `packages/common/constructs`, called `UserIdentity`.
|
|
31
|
+
|
|
32
|
+
:::note
|
|
33
|
+
The PDK `CloudscapeReactTsWebsiteProject` relied on `@aws-northstar/ui` to provide Cognito login components in-app. If you would prefer to use this approach instead of moving to the hosted UI, you can replace the generated `packages/common/constructs/src/core/user-identity.ts` with the [implementation from PDK](https://github.com/aws/aws-pdk/blob/mainline/packages/identity/src/user-identity.ts), replacing the constructs from `@aws-cdk/aws-cognito-identitypool-alpha` with those vended by `aws-cdk-lib`, and make sure to set the relevant values in `RuntimeConfig`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
RuntimeConfig.ensure(this).config = {
|
|
37
|
+
...RuntimeConfig.ensure(this).config,
|
|
38
|
+
region: Stack.of(this).region,
|
|
39
|
+
identityPoolId: this.identityPool.identityPoolId,
|
|
40
|
+
userPoolId: this.userPool.userPoolId,
|
|
41
|
+
userPoolWebClientId: this.userPoolClient.userPoolClientId,
|
|
42
|
+
};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You can then reuse the `Auth` component from your `CloudscapeReactTsWebsiteProject`.
|
|
46
|
+
:::
|
|
47
|
+
|
|
48
|
+
#### Connect the Website to the API
|
|
49
|
+
|
|
50
|
+
In PDK you could pass the vended Projen projects to one another to trigger integration code to be vended. This was used in the shopping list application to configure the website to be able to integrate with the API.
|
|
51
|
+
|
|
52
|
+
With the Nx Plugin for AWS, API integration is supported via the <Link path="/guides/connection">`connection` generator</Link>. Next, we use this generator so that our website can invoke our Smithy API:
|
|
53
|
+
|
|
54
|
+
<RunGenerator generator="connection" noInteractive requiredParameters={{ sourceProject: 'website', targetProject: 'api' }} />
|
|
55
|
+
|
|
56
|
+
This generates the necessary client providers and build targets for your website to call your API via a generated TypeScript client.
|
|
57
|
+
|
|
58
|
+
#### Add AWS Northstar Dependency
|
|
59
|
+
|
|
60
|
+
The `CloudscapeReactTsWebsiteProject` automatically included a dependency on `@aws-northstar/ui` which is used in our shopping list application, so we add it here:
|
|
61
|
+
|
|
62
|
+
<InstallCommand pkg="@aws-northstar/ui" />
|
|
63
|
+
|
|
64
|
+
#### Move the Components and Pages
|
|
65
|
+
|
|
66
|
+
The [shopping list application](https://aws.github.io/aws-pdk/getting_started/shopping_list_app.html#create-new-pages-components) has one component called `CreateItem`, and two pages, `ShoppingList` and `ShoppingLists`. We'll migrate these to the new website, making a few adjustments since we're using TanStack Router and the Nx Plugin for AWS TypeScript client code generator.
|
|
67
|
+
|
|
68
|
+
<Steps>
|
|
69
|
+
|
|
70
|
+
1. Copy `packages/website/src/components/CreateItem/index.tsx` from the PDK project into the exact same location in the new project.
|
|
71
|
+
|
|
72
|
+
1. Copy `packages/website/src/pages/ShoppingLists/index.tsx` to `packages/website/src/routes/index.tsx`, since `ShoppingLists` is our home page and we use file-based routing with TanStack router.
|
|
73
|
+
|
|
74
|
+
1. Copy `packages/website/src/pages/ShoppingList/index.tsx` to `packages/website/src/routes/$shoppingListId.tsx`, since `ShoppingList` was the page we want to show on the `/:shoppingListId` route.
|
|
75
|
+
|
|
76
|
+
</Steps>
|
|
77
|
+
|
|
78
|
+
Note that you'll now have some build errors visible in your IDE, we'll need to make a few more changes to fit within the new framework, outlined below.
|
|
79
|
+
|
|
80
|
+
#### Migrate from React Router to TanStack Router
|
|
81
|
+
|
|
82
|
+
Since we're using [file-based routing](https://tanstack.com/router/latest/docs/framework/react/routing/file-based-routing), we can use the website local development server to manage automatically generating route configuration. Let's start the local website server:
|
|
83
|
+
|
|
84
|
+
<NxCommands commands={["serve-local website"]} />
|
|
85
|
+
|
|
86
|
+
:::tip
|
|
87
|
+
We're using the `serve-local` target here, which also starts local servers for any APIs which have been connected with `connection`, and hot-reloads if your website, model, or backend changes! This allows us to test our API and website locally before we've even written any CDK code.
|
|
88
|
+
:::
|
|
89
|
+
|
|
90
|
+
You'll see some errors, but the local website server should start on port `4200`, as well as the local Smithy API server on port `3001`.
|
|
91
|
+
|
|
92
|
+
Follow the below steps in both `routes/index.tsx` and `routes/$shoppingListId.tsx` to migrate to TanStack Router:
|
|
93
|
+
|
|
94
|
+
<Steps>
|
|
95
|
+
|
|
96
|
+
1. Add `createFileRoute` to register each route:
|
|
97
|
+
|
|
98
|
+
<Tabs>
|
|
99
|
+
<TabItem label="routes/index.tsx">
|
|
100
|
+
```diff lang="ts"
|
|
101
|
+
+import { createFileRoute } from "@tanstack/react-router";
|
|
102
|
+
...
|
|
103
|
+
-export default ShoppingLists;
|
|
104
|
+
+export const Route = createFileRoute('/')({
|
|
105
|
+
+ component: ShoppingLists,
|
|
106
|
+
+});
|
|
107
|
+
```
|
|
108
|
+
</TabItem>
|
|
109
|
+
<TabItem label="routes/$shoppingListId.tsx">
|
|
110
|
+
```diff lang="ts"
|
|
111
|
+
+import { createFileRoute } from "@tanstack/react-router";
|
|
112
|
+
...
|
|
113
|
+
-export default ShoppingList;
|
|
114
|
+
+export const Route = createFileRoute('/$shoppingListId')({
|
|
115
|
+
+ component: ShoppingList,
|
|
116
|
+
+});
|
|
117
|
+
```
|
|
118
|
+
</TabItem>
|
|
119
|
+
</Tabs>
|
|
120
|
+
|
|
121
|
+
After you save the file, you'll notice that type errors with call to `createFileRoute` have gone.
|
|
122
|
+
|
|
123
|
+
1. Replace the `useNavigate` hook.
|
|
124
|
+
|
|
125
|
+
Update the import:
|
|
126
|
+
|
|
127
|
+
```diff lang="ts"
|
|
128
|
+
- import { useNavigate } from 'react-router-dom';
|
|
129
|
+
+ import { useNavigate } from '@tanstack/react-router';
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Update calls to the `navigate` method (returned by `useNavigate`) to pass in the type-safe routes:
|
|
133
|
+
|
|
134
|
+
```diff lang="ts"
|
|
135
|
+
- navigate(`/${cell.shoppingListId}`);
|
|
136
|
+
+ navigate({
|
|
137
|
+
+ to: '/$shoppingListId',
|
|
138
|
+
+ params: { shoppingListId: cell.shoppingListId },
|
|
139
|
+
+ });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
1. Replace the `useParams` hook.
|
|
143
|
+
|
|
144
|
+
Remove the import:
|
|
145
|
+
|
|
146
|
+
```diff lang="ts"
|
|
147
|
+
- import { useParams } from 'react-router-dom';
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Update calls to `useParams` with the hook provided by the `Route` created above. These are now type-safe!
|
|
151
|
+
|
|
152
|
+
```diff lang="ts"
|
|
153
|
+
- const { shoppingListId } = useParams();
|
|
154
|
+
+ const { shoppingListId } = Route.useParams();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
</Steps>
|
|
158
|
+
|
|
159
|
+
:::tip
|
|
160
|
+
You can also check out the [detailed guide in the TanStack Router documentation](https://tanstack.com/router/latest/docs/installation/migrate-from-react-router) for more in-depth information for more complex scenarios.
|
|
161
|
+
:::
|
|
162
|
+
|
|
163
|
+
#### Fix Component Imports
|
|
164
|
+
|
|
165
|
+
Since our route files aren't as deeply nested in the file tree as they were in our PDK project, we need to fix the import for `CreateItem` in both `routes/index.tsx` and `routes/$shoppingListId.tsx`:
|
|
166
|
+
|
|
167
|
+
```diff lang="ts"
|
|
168
|
+
-import CreateItem from "../../components/CreateItem";
|
|
169
|
+
+import CreateItem from "../components/CreateItem";
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The `AppLayoutContext` is also vended in a slightly different location in our new project:
|
|
173
|
+
|
|
174
|
+
```diff lang="ts"
|
|
175
|
+
-import { AppLayoutContext } from "../../layouts/App";
|
|
176
|
+
+import { AppLayoutContext } from "../components/AppLayout";
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### Migrate to use the new Generated TypeScript Client
|
|
180
|
+
|
|
181
|
+
We're getting closer now! Next, we need to migrate to use the TypeScript client vended by the Nx Plugin for AWS, which has a few improvements compared to Type Safe API. To achieve this, follow the below steps
|
|
182
|
+
|
|
183
|
+
<Steps>
|
|
184
|
+
|
|
185
|
+
1. Import the new generated client and types instead of the old, for example:
|
|
186
|
+
|
|
187
|
+
```diff lang="ts"
|
|
188
|
+
-import {
|
|
189
|
+
- ShoppingList,
|
|
190
|
+
- usePutShoppingList,
|
|
191
|
+
- useDeleteShoppingList,
|
|
192
|
+
- useGetShoppingLists,
|
|
193
|
+
-} from "myapi-typescript-react-query-hooks";
|
|
194
|
+
+import { ShoppingList } from "../generated/my-api/types.gen";
|
|
195
|
+
+import { useMyApi } from "../hooks/useMyApi";
|
|
196
|
+
+import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Note that `routes/$shoppingListId.tsx` imports the `ShoppingList` type as `_ShoppingList` - in that file we should do the same, but again importing from `types.gen`.
|
|
200
|
+
|
|
201
|
+
Note also we import the relevant hooks directly from `@tanstack/react-query`, since the generated client provides methods to generate options for TanStack query hooks, rather than hook wrappers.
|
|
202
|
+
|
|
203
|
+
1. Instantiate the new TanStack Query hooks, for example:
|
|
204
|
+
|
|
205
|
+
```diff lang="ts"
|
|
206
|
+
-const getShoppingLists = useGetShoppingLists({ pageSize: PAGE_SIZE });
|
|
207
|
+
-const putShoppingList = usePutShoppingList();
|
|
208
|
+
-const deleteShoppingList = useDeleteShoppingList();
|
|
209
|
+
+const api = useMyApi();
|
|
210
|
+
+const getShoppingLists = useInfiniteQuery(
|
|
211
|
+
+ api.getShoppingLists.infiniteQueryOptions(
|
|
212
|
+
+ { pageSize: PAGE_SIZE },
|
|
213
|
+
+ { getNextPageParam: (p) => p.nextToken },
|
|
214
|
+
+ ),
|
|
215
|
+
+);
|
|
216
|
+
+const putShoppingList = useMutation(api.putShoppingList.mutationOptions());
|
|
217
|
+
+const deleteShoppingList = useMutation(
|
|
218
|
+
+ api.deleteShoppingList.mutationOptions(),
|
|
219
|
+
+);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
1. Remove the wrapper `<operation>RequestContent` for calls to operations which accept parameters in the request body:
|
|
223
|
+
|
|
224
|
+
```diff lang="ts"
|
|
225
|
+
await putShoppingList.mutateAsync({
|
|
226
|
+
- putShoppingListRequestContent: {
|
|
227
|
+
name: item,
|
|
228
|
+
- },
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
</Steps>
|
|
233
|
+
|
|
234
|
+
:::note
|
|
235
|
+
There was also a minor bug in the shopping list application's `pages/ShoppingList/index.tsx` where it was not using optional chaining correctly. Let's fix this too to avoid a lint error when we run our final build:
|
|
236
|
+
|
|
237
|
+
```diff lang="ts"
|
|
238
|
+
const shoppingList: _ShoppingList | undefined =
|
|
239
|
+
- getShoppingLists.data?.pages[0].shoppingLists[0]!;
|
|
240
|
+
+ getShoppingLists.data?.pages?.[0]?.shoppingLists?.[0];
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
And likewise update the references which assumed it's defined:
|
|
244
|
+
|
|
245
|
+
```diff lang="ts"
|
|
246
|
+
-name: shoppingList.name,
|
|
247
|
+
-shoppingListId: shoppingList.shoppingListId,
|
|
248
|
+
+name: shoppingList?.name ?? 'my list',
|
|
249
|
+
+shoppingListId: shoppingList?.shoppingListId,
|
|
250
|
+
```
|
|
251
|
+
:::
|
|
252
|
+
|
|
253
|
+
#### Migrate from TanStack Query v4 to v5
|
|
254
|
+
|
|
255
|
+
There are a few errors left to fix due to differences between TanStack Query v4 (used by PDK) and v5 which the `connection` generator added:
|
|
256
|
+
|
|
257
|
+
<Steps>
|
|
258
|
+
|
|
259
|
+
1. Replace `isLoading` with `isPending` for mutations, for example:
|
|
260
|
+
|
|
261
|
+
```diff lang="ts"
|
|
262
|
+
-putShoppingList.isLoading
|
|
263
|
+
+putShoppingList.isPending
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
1. The shopping list application made use of the `InfiniteQueryTable` from `@aws-northstar/ui` which expects a type from TanStack Query v4. This actually works with infinite queries from v5, so we can just suppress the type error:
|
|
267
|
+
|
|
268
|
+
```diff lang="tsx"
|
|
269
|
+
<InfiniteQueryTable
|
|
270
|
+
- query={getShoppingLists}
|
|
271
|
+
+ query={getShoppingLists as any}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
</Steps>
|
|
275
|
+
|
|
276
|
+
:::tip
|
|
277
|
+
The TanStack Query documentation has a [detailed migration guide](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5) which you can follow if you have used more features of the library.
|
|
278
|
+
:::
|
|
279
|
+
|
|
280
|
+
#### Visit the Local Website
|
|
281
|
+
|
|
282
|
+
You can now visit the local website at http://localhost:4200/
|
|
283
|
+
|
|
284
|
+
The website should load up now that everything's been migrated! Since the only infrastructure that the shopping list application relies on besides API, Website and Identity is the DynamoDB table - if you've got a DynamoDB table named `shopping_list` in-region, and local AWS credentials which can access it, the website will be fully functional!
|
|
285
|
+
|
|
286
|
+
If not, that's ok, we'll migrate the infrastructure next.
|
|
287
|
+
|
|
288
|
+
<Drawer title="Shopping List Page Migration" trigger="Click here for full before/after examples for the two shopping list pages from the tutorial">
|
|
289
|
+
|
|
290
|
+
<h4>Shopping Lists Page</h4>
|
|
291
|
+
|
|
292
|
+
<Tabs syncKey="pdk-migration">
|
|
293
|
+
<TabItem label="Before">
|
|
294
|
+
```tsx
|
|
295
|
+
// pages/ShoppingLists/index.tsx
|
|
296
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
297
|
+
import { InfiniteQueryTable } from "@aws-northstar/ui/components";
|
|
298
|
+
import {
|
|
299
|
+
Button,
|
|
300
|
+
Header,
|
|
301
|
+
Link,
|
|
302
|
+
SpaceBetween,
|
|
303
|
+
TableProps,
|
|
304
|
+
} from "@cloudscape-design/components";
|
|
305
|
+
import {
|
|
306
|
+
ShoppingList,
|
|
307
|
+
usePutShoppingList,
|
|
308
|
+
useDeleteShoppingList,
|
|
309
|
+
useGetShoppingLists,
|
|
310
|
+
} from "myapi-typescript-react-query-hooks";
|
|
311
|
+
import { useContext, useEffect, useMemo, useState } from "react";
|
|
312
|
+
import { useNavigate } from "react-router-dom";
|
|
313
|
+
import CreateItem from "../../components/CreateItem";
|
|
314
|
+
import { AppLayoutContext } from "../../layouts/App";
|
|
315
|
+
|
|
316
|
+
const PAGE_SIZE = 50;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Component to render the ShoppingLists "/" route.
|
|
320
|
+
*/
|
|
321
|
+
const ShoppingLists: React.FC = () => {
|
|
322
|
+
const [visibleModal, setVisibleModal] = useState(false);
|
|
323
|
+
const [selectedShoppingList, setSelectedShoppingList] = useState<
|
|
324
|
+
ShoppingList[]
|
|
325
|
+
>([]);
|
|
326
|
+
const getShoppingLists = useGetShoppingLists({ pageSize: PAGE_SIZE });
|
|
327
|
+
const putShoppingList = usePutShoppingList();
|
|
328
|
+
const deleteShoppingList = useDeleteShoppingList();
|
|
329
|
+
const navigate = useNavigate();
|
|
330
|
+
const { setAppLayoutProps } = useContext(AppLayoutContext);
|
|
331
|
+
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
setAppLayoutProps({
|
|
334
|
+
contentType: "table",
|
|
335
|
+
});
|
|
336
|
+
}, [setAppLayoutProps]);
|
|
337
|
+
|
|
338
|
+
const columnDefinitions = useMemo<
|
|
339
|
+
TableProps.ColumnDefinition<ShoppingList>[]
|
|
340
|
+
>(
|
|
341
|
+
() => [
|
|
342
|
+
{
|
|
343
|
+
id: "shoppingListId",
|
|
344
|
+
isRowHeader: true,
|
|
345
|
+
header: "Shopping List Id",
|
|
346
|
+
cell: (cell) => (
|
|
347
|
+
<Link
|
|
348
|
+
href={`/${cell.shoppingListId}`}
|
|
349
|
+
onFollow={(e) => {
|
|
350
|
+
e.preventDefault();
|
|
351
|
+
navigate(`/${cell.shoppingListId}`);
|
|
352
|
+
}}
|
|
353
|
+
>
|
|
354
|
+
{cell.shoppingListId}
|
|
355
|
+
</Link>
|
|
356
|
+
),
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
id: "name",
|
|
360
|
+
header: "Name",
|
|
361
|
+
cell: (cell) => cell.name,
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
id: "shoppingItems",
|
|
365
|
+
header: "Shopping Items",
|
|
366
|
+
cell: (cell) => `${cell.shoppingItems?.length || 0} Items.`,
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
[navigate],
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<>
|
|
374
|
+
<CreateItem
|
|
375
|
+
title="Create Shopping List"
|
|
376
|
+
callback={async (item) => {
|
|
377
|
+
await putShoppingList.mutateAsync({
|
|
378
|
+
putShoppingListRequestContent: {
|
|
379
|
+
name: item,
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
getShoppingLists.refetch();
|
|
383
|
+
}}
|
|
384
|
+
isLoading={putShoppingList.isLoading}
|
|
385
|
+
visibleModal={visibleModal}
|
|
386
|
+
setVisibleModal={setVisibleModal}
|
|
387
|
+
/>
|
|
388
|
+
<InfiniteQueryTable
|
|
389
|
+
query={getShoppingLists}
|
|
390
|
+
itemsKey="shoppingLists"
|
|
391
|
+
pageSize={PAGE_SIZE}
|
|
392
|
+
selectionType="single"
|
|
393
|
+
stickyHeader={true}
|
|
394
|
+
selectedItems={selectedShoppingList}
|
|
395
|
+
onSelectionChange={(e) =>
|
|
396
|
+
setSelectedShoppingList(e.detail.selectedItems)
|
|
397
|
+
}
|
|
398
|
+
header={
|
|
399
|
+
<Header
|
|
400
|
+
variant="awsui-h1-sticky"
|
|
401
|
+
actions={
|
|
402
|
+
<SpaceBetween size="xs" direction="horizontal">
|
|
403
|
+
<Button
|
|
404
|
+
loading={deleteShoppingList.isLoading}
|
|
405
|
+
data-testid="header-btn-delete"
|
|
406
|
+
disabled={selectedShoppingList.length === 0}
|
|
407
|
+
onClick={async () => {
|
|
408
|
+
await deleteShoppingList.mutateAsync({
|
|
409
|
+
shoppingListId: selectedShoppingList![0].shoppingListId,
|
|
410
|
+
});
|
|
411
|
+
setSelectedShoppingList([]);
|
|
412
|
+
getShoppingLists.refetch();
|
|
413
|
+
}}
|
|
414
|
+
>
|
|
415
|
+
Delete
|
|
416
|
+
</Button>
|
|
417
|
+
<Button
|
|
418
|
+
data-testid="header-btn-create"
|
|
419
|
+
variant="primary"
|
|
420
|
+
onClick={() => setVisibleModal(true)}
|
|
421
|
+
>
|
|
422
|
+
Create Shopping List
|
|
423
|
+
</Button>
|
|
424
|
+
</SpaceBetween>
|
|
425
|
+
}
|
|
426
|
+
>
|
|
427
|
+
Shopping Lists
|
|
428
|
+
</Header>
|
|
429
|
+
}
|
|
430
|
+
variant="full-page"
|
|
431
|
+
columnDefinitions={columnDefinitions}
|
|
432
|
+
/>
|
|
433
|
+
</>
|
|
434
|
+
);
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
export default ShoppingLists;
|
|
438
|
+
```
|
|
439
|
+
</TabItem>
|
|
440
|
+
<TabItem label="After">
|
|
441
|
+
```tsx
|
|
442
|
+
// routes/index.tsx
|
|
443
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
444
|
+
import { InfiniteQueryTable } from "@aws-northstar/ui/components";
|
|
445
|
+
import {
|
|
446
|
+
Button,
|
|
447
|
+
Header,
|
|
448
|
+
Link,
|
|
449
|
+
SpaceBetween,
|
|
450
|
+
TableProps,
|
|
451
|
+
} from "@cloudscape-design/components";
|
|
452
|
+
import { useContext, useEffect, useMemo, useState } from "react";
|
|
453
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
454
|
+
import CreateItem from "../components/CreateItem";
|
|
455
|
+
import { AppLayoutContext } from "../components/AppLayout";
|
|
456
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
457
|
+
import { ShoppingList } from "../generated/my-api/types.gen";
|
|
458
|
+
import { useMyApi } from "../hooks/useMyApi";
|
|
459
|
+
import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
|
|
460
|
+
|
|
461
|
+
const PAGE_SIZE = 50;
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Component to render the ShoppingLists "/" route.
|
|
465
|
+
*/
|
|
466
|
+
const ShoppingLists: React.FC = () => {
|
|
467
|
+
const [visibleModal, setVisibleModal] = useState(false);
|
|
468
|
+
const [selectedShoppingList, setSelectedShoppingList] = useState<
|
|
469
|
+
ShoppingList[]
|
|
470
|
+
>([]);
|
|
471
|
+
const api = useMyApi();
|
|
472
|
+
const getShoppingLists = useInfiniteQuery(
|
|
473
|
+
api.getShoppingLists.infiniteQueryOptions(
|
|
474
|
+
{ pageSize: PAGE_SIZE },
|
|
475
|
+
{ getNextPageParam: (res) => res.nextToken },
|
|
476
|
+
),
|
|
477
|
+
);
|
|
478
|
+
const putShoppingList = useMutation(api.putShoppingList.mutationOptions());
|
|
479
|
+
const deleteShoppingList = useMutation(
|
|
480
|
+
api.deleteShoppingList.mutationOptions(),
|
|
481
|
+
);
|
|
482
|
+
const navigate = useNavigate();
|
|
483
|
+
const { setAppLayoutProps } = useContext(AppLayoutContext);
|
|
484
|
+
|
|
485
|
+
useEffect(() => {
|
|
486
|
+
setAppLayoutProps({
|
|
487
|
+
contentType: "table",
|
|
488
|
+
});
|
|
489
|
+
}, [setAppLayoutProps]);
|
|
490
|
+
|
|
491
|
+
const columnDefinitions = useMemo<
|
|
492
|
+
TableProps.ColumnDefinition<ShoppingList>[]
|
|
493
|
+
>(
|
|
494
|
+
() => [
|
|
495
|
+
{
|
|
496
|
+
id: "shoppingListId",
|
|
497
|
+
isRowHeader: true,
|
|
498
|
+
header: "Shopping List Id",
|
|
499
|
+
cell: (cell) => (
|
|
500
|
+
<Link
|
|
501
|
+
href={`/${cell.shoppingListId}`}
|
|
502
|
+
onFollow={(e) => {
|
|
503
|
+
e.preventDefault();
|
|
504
|
+
navigate({
|
|
505
|
+
to: '/$shoppingListId',
|
|
506
|
+
params: { shoppingListId: cell.shoppingListId },
|
|
507
|
+
});
|
|
508
|
+
}}
|
|
509
|
+
>
|
|
510
|
+
{cell.shoppingListId}
|
|
511
|
+
</Link>
|
|
512
|
+
),
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
id: "name",
|
|
516
|
+
header: "Name",
|
|
517
|
+
cell: (cell) => cell.name,
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
id: "shoppingItems",
|
|
521
|
+
header: "Shopping Items",
|
|
522
|
+
cell: (cell) => `${cell.shoppingItems?.length || 0} Items.`,
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
[navigate],
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
return (
|
|
529
|
+
<>
|
|
530
|
+
<CreateItem
|
|
531
|
+
title="Create Shopping List"
|
|
532
|
+
callback={async (item) => {
|
|
533
|
+
await putShoppingList.mutateAsync({
|
|
534
|
+
name: item,
|
|
535
|
+
});
|
|
536
|
+
getShoppingLists.refetch();
|
|
537
|
+
}}
|
|
538
|
+
isLoading={putShoppingList.isPending}
|
|
539
|
+
visibleModal={visibleModal}
|
|
540
|
+
setVisibleModal={setVisibleModal}
|
|
541
|
+
/>
|
|
542
|
+
<InfiniteQueryTable
|
|
543
|
+
query={getShoppingLists as any}
|
|
544
|
+
itemsKey="shoppingLists"
|
|
545
|
+
pageSize={PAGE_SIZE}
|
|
546
|
+
selectionType="single"
|
|
547
|
+
stickyHeader={true}
|
|
548
|
+
selectedItems={selectedShoppingList}
|
|
549
|
+
onSelectionChange={(e) =>
|
|
550
|
+
setSelectedShoppingList(e.detail.selectedItems)
|
|
551
|
+
}
|
|
552
|
+
header={
|
|
553
|
+
<Header
|
|
554
|
+
variant="awsui-h1-sticky"
|
|
555
|
+
actions={
|
|
556
|
+
<SpaceBetween size="xs" direction="horizontal">
|
|
557
|
+
<Button
|
|
558
|
+
loading={deleteShoppingList.isPending}
|
|
559
|
+
data-testid="header-btn-delete"
|
|
560
|
+
disabled={selectedShoppingList.length === 0}
|
|
561
|
+
onClick={async () => {
|
|
562
|
+
await deleteShoppingList.mutateAsync({
|
|
563
|
+
shoppingListId: selectedShoppingList![0].shoppingListId,
|
|
564
|
+
});
|
|
565
|
+
setSelectedShoppingList([]);
|
|
566
|
+
getShoppingLists.refetch();
|
|
567
|
+
}}
|
|
568
|
+
>
|
|
569
|
+
Delete
|
|
570
|
+
</Button>
|
|
571
|
+
<Button
|
|
572
|
+
data-testid="header-btn-create"
|
|
573
|
+
variant="primary"
|
|
574
|
+
onClick={() => setVisibleModal(true)}
|
|
575
|
+
>
|
|
576
|
+
Create Shopping List
|
|
577
|
+
</Button>
|
|
578
|
+
</SpaceBetween>
|
|
579
|
+
}
|
|
580
|
+
>
|
|
581
|
+
Shopping Lists
|
|
582
|
+
</Header>
|
|
583
|
+
}
|
|
584
|
+
variant="full-page"
|
|
585
|
+
columnDefinitions={columnDefinitions}
|
|
586
|
+
/>
|
|
587
|
+
</>
|
|
588
|
+
);
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
export const Route = createFileRoute('/')({
|
|
592
|
+
component: ShoppingLists,
|
|
593
|
+
});
|
|
594
|
+
```
|
|
595
|
+
</TabItem>
|
|
596
|
+
</Tabs>
|
|
597
|
+
|
|
598
|
+
<h4>Shopping List Page</h4>
|
|
599
|
+
|
|
600
|
+
<Tabs syncKey="pdk-migration">
|
|
601
|
+
<TabItem label="Before">
|
|
602
|
+
```tsx
|
|
603
|
+
// pages/ShoppingList/index.tsx
|
|
604
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
605
|
+
import {
|
|
606
|
+
Board,
|
|
607
|
+
BoardItem,
|
|
608
|
+
BoardProps,
|
|
609
|
+
} from "@cloudscape-design/board-components";
|
|
610
|
+
import {
|
|
611
|
+
Button,
|
|
612
|
+
Container,
|
|
613
|
+
ContentLayout,
|
|
614
|
+
Header,
|
|
615
|
+
SpaceBetween,
|
|
616
|
+
Spinner,
|
|
617
|
+
} from "@cloudscape-design/components";
|
|
618
|
+
import {
|
|
619
|
+
ShoppingList as _ShoppingList,
|
|
620
|
+
usePutShoppingList,
|
|
621
|
+
useGetShoppingLists,
|
|
622
|
+
} from "myapi-typescript-react-query-hooks";
|
|
623
|
+
import { useEffect, useState } from "react";
|
|
624
|
+
import { useParams } from "react-router-dom";
|
|
625
|
+
import CreateItem from "../../components/CreateItem";
|
|
626
|
+
|
|
627
|
+
type ListItem = { name: string };
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Component to render a singular Shopping List "/:shoppingListId" route.
|
|
631
|
+
*/
|
|
632
|
+
const ShoppingList: React.FC = () => {
|
|
633
|
+
const { shoppingListId } = useParams();
|
|
634
|
+
const [visibleModal, setVisibleModal] = useState(false);
|
|
635
|
+
const getShoppingLists = useGetShoppingLists({ shoppingListId });
|
|
636
|
+
const putShoppingList = usePutShoppingList();
|
|
637
|
+
const shoppingList: _ShoppingList | undefined =
|
|
638
|
+
getShoppingLists.data?.pages[0].shoppingLists[0]!;
|
|
639
|
+
const [shoppingItems, setShoppingItems] =
|
|
640
|
+
useState<BoardProps.Item<ListItem>[]>();
|
|
641
|
+
|
|
642
|
+
useEffect(() => {
|
|
643
|
+
setShoppingItems(
|
|
644
|
+
shoppingList?.shoppingItems?.map((i) => ({
|
|
645
|
+
id: i,
|
|
646
|
+
definition: { minColumnSpan: 4 },
|
|
647
|
+
data: { name: i },
|
|
648
|
+
})),
|
|
649
|
+
);
|
|
650
|
+
}, [shoppingList?.shoppingItems]);
|
|
651
|
+
|
|
652
|
+
return (
|
|
653
|
+
<ContentLayout
|
|
654
|
+
header={
|
|
655
|
+
<Header
|
|
656
|
+
variant="awsui-h1-sticky"
|
|
657
|
+
actions={
|
|
658
|
+
<SpaceBetween size="xs" direction="horizontal">
|
|
659
|
+
<Button
|
|
660
|
+
data-testid="header-btn-create"
|
|
661
|
+
variant="primary"
|
|
662
|
+
onClick={() => setVisibleModal(true)}
|
|
663
|
+
>
|
|
664
|
+
Add Item
|
|
665
|
+
</Button>
|
|
666
|
+
</SpaceBetween>
|
|
667
|
+
}
|
|
668
|
+
>
|
|
669
|
+
Shopping list: {shoppingList?.name}
|
|
670
|
+
</Header>
|
|
671
|
+
}
|
|
672
|
+
>
|
|
673
|
+
<CreateItem
|
|
674
|
+
isLoading={false}
|
|
675
|
+
title="Add Item"
|
|
676
|
+
callback={async (item) => {
|
|
677
|
+
const items = [
|
|
678
|
+
...(shoppingItems || []),
|
|
679
|
+
{
|
|
680
|
+
id: item,
|
|
681
|
+
definition: { minColumnSpan: 4 },
|
|
682
|
+
data: { name: item },
|
|
683
|
+
},
|
|
684
|
+
];
|
|
685
|
+
setShoppingItems(items);
|
|
686
|
+
putShoppingList.mutate({
|
|
687
|
+
putShoppingListRequestContent: {
|
|
688
|
+
name: shoppingList.name,
|
|
689
|
+
shoppingListId: shoppingList.shoppingListId,
|
|
690
|
+
shoppingItems: items.map((i) => i.data.name),
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
}}
|
|
694
|
+
visibleModal={visibleModal}
|
|
695
|
+
setVisibleModal={setVisibleModal}
|
|
696
|
+
/>
|
|
697
|
+
<Container>
|
|
698
|
+
{!shoppingList ? (
|
|
699
|
+
<Spinner />
|
|
700
|
+
) : (
|
|
701
|
+
<Board<ListItem>
|
|
702
|
+
onItemsChange={(event) => {
|
|
703
|
+
const items = event.detail.items as BoardProps.Item<ListItem>[];
|
|
704
|
+
setShoppingItems(items);
|
|
705
|
+
putShoppingList.mutate({
|
|
706
|
+
putShoppingListRequestContent: {
|
|
707
|
+
name: shoppingList.name,
|
|
708
|
+
shoppingListId: shoppingList.shoppingListId,
|
|
709
|
+
shoppingItems: items.map((i) => i.data.name),
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
}}
|
|
713
|
+
items={shoppingItems || []}
|
|
714
|
+
renderItem={(item, actions) => (
|
|
715
|
+
<BoardItem
|
|
716
|
+
header={item.data.name}
|
|
717
|
+
settings={
|
|
718
|
+
<Button
|
|
719
|
+
iconName="close"
|
|
720
|
+
variant="icon"
|
|
721
|
+
onClick={actions.removeItem}
|
|
722
|
+
/>
|
|
723
|
+
}
|
|
724
|
+
i18nStrings={{
|
|
725
|
+
dragHandleAriaLabel: "Drag handle",
|
|
726
|
+
dragHandleAriaDescription:
|
|
727
|
+
"Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard.",
|
|
728
|
+
resizeHandleAriaLabel: "Resize handle",
|
|
729
|
+
resizeHandleAriaDescription:
|
|
730
|
+
"Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard.",
|
|
731
|
+
}}
|
|
732
|
+
/>
|
|
733
|
+
)}
|
|
734
|
+
i18nStrings={{
|
|
735
|
+
liveAnnouncementDndCommitted: () => "",
|
|
736
|
+
liveAnnouncementDndDiscarded: () => "",
|
|
737
|
+
liveAnnouncementDndItemInserted: () => "",
|
|
738
|
+
liveAnnouncementDndItemReordered: () => "",
|
|
739
|
+
liveAnnouncementDndItemResized: () => "",
|
|
740
|
+
liveAnnouncementDndStarted: () => "",
|
|
741
|
+
liveAnnouncementItemRemoved: () => "",
|
|
742
|
+
navigationAriaLabel: "",
|
|
743
|
+
navigationItemAriaLabel: () => "",
|
|
744
|
+
}}
|
|
745
|
+
empty={<></>}
|
|
746
|
+
/>
|
|
747
|
+
)}
|
|
748
|
+
</Container>
|
|
749
|
+
</ContentLayout>
|
|
750
|
+
);
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
export default ShoppingList;
|
|
754
|
+
```
|
|
755
|
+
</TabItem>
|
|
756
|
+
<TabItem label="After">
|
|
757
|
+
```tsx
|
|
758
|
+
// routes/$shoppingListId.tsx
|
|
759
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
760
|
+
import {
|
|
761
|
+
Board,
|
|
762
|
+
BoardItem,
|
|
763
|
+
BoardProps,
|
|
764
|
+
} from "@cloudscape-design/board-components";
|
|
765
|
+
import {
|
|
766
|
+
Button,
|
|
767
|
+
Container,
|
|
768
|
+
ContentLayout,
|
|
769
|
+
Header,
|
|
770
|
+
SpaceBetween,
|
|
771
|
+
Spinner,
|
|
772
|
+
} from "@cloudscape-design/components";
|
|
773
|
+
import { useEffect, useState } from "react";
|
|
774
|
+
import CreateItem from "../components/CreateItem";
|
|
775
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
776
|
+
import { useMyApi } from "../hooks/useMyApi";
|
|
777
|
+
import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
|
|
778
|
+
import { ShoppingList as _ShoppingList } from "../generated/my-api/types.gen";
|
|
779
|
+
|
|
780
|
+
type ListItem = { name: string };
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Component to render a singular Shopping List "/:shoppingListId" route.
|
|
784
|
+
*/
|
|
785
|
+
const ShoppingList: React.FC = () => {
|
|
786
|
+
const { shoppingListId } = Route.useParams();
|
|
787
|
+
const [visibleModal, setVisibleModal] = useState(false);
|
|
788
|
+
const api = useMyApi();
|
|
789
|
+
const getShoppingLists = useInfiniteQuery(
|
|
790
|
+
api.getShoppingLists.infiniteQueryOptions(
|
|
791
|
+
{ shoppingListId },
|
|
792
|
+
{ getNextPageParam: (p) => p.nextToken },
|
|
793
|
+
),
|
|
794
|
+
);
|
|
795
|
+
const putShoppingList = useMutation(api.putShoppingList.mutationOptions());
|
|
796
|
+
const shoppingList: _ShoppingList | undefined =
|
|
797
|
+
getShoppingLists.data?.pages?.[0]?.shoppingLists?.[0];
|
|
798
|
+
const [shoppingItems, setShoppingItems] =
|
|
799
|
+
useState<BoardProps.Item<ListItem>[]>();
|
|
800
|
+
|
|
801
|
+
useEffect(() => {
|
|
802
|
+
setShoppingItems(
|
|
803
|
+
shoppingList?.shoppingItems?.map((i) => ({
|
|
804
|
+
id: i,
|
|
805
|
+
definition: { minColumnSpan: 4 },
|
|
806
|
+
data: { name: i },
|
|
807
|
+
})),
|
|
808
|
+
);
|
|
809
|
+
}, [shoppingList?.shoppingItems]);
|
|
810
|
+
|
|
811
|
+
return (
|
|
812
|
+
<ContentLayout
|
|
813
|
+
header={
|
|
814
|
+
<Header
|
|
815
|
+
variant="awsui-h1-sticky"
|
|
816
|
+
actions={
|
|
817
|
+
<SpaceBetween size="xs" direction="horizontal">
|
|
818
|
+
<Button
|
|
819
|
+
data-testid="header-btn-create"
|
|
820
|
+
variant="primary"
|
|
821
|
+
onClick={() => setVisibleModal(true)}
|
|
822
|
+
>
|
|
823
|
+
Add Item
|
|
824
|
+
</Button>
|
|
825
|
+
</SpaceBetween>
|
|
826
|
+
}
|
|
827
|
+
>
|
|
828
|
+
Shopping list: {shoppingList?.name}
|
|
829
|
+
</Header>
|
|
830
|
+
}
|
|
831
|
+
>
|
|
832
|
+
<CreateItem
|
|
833
|
+
isLoading={false}
|
|
834
|
+
title="Add Item"
|
|
835
|
+
callback={async (item) => {
|
|
836
|
+
const items = [
|
|
837
|
+
...(shoppingItems || []),
|
|
838
|
+
{
|
|
839
|
+
id: item,
|
|
840
|
+
definition: { minColumnSpan: 4 },
|
|
841
|
+
data: { name: item },
|
|
842
|
+
},
|
|
843
|
+
];
|
|
844
|
+
setShoppingItems(items);
|
|
845
|
+
putShoppingList.mutate({
|
|
846
|
+
name: shoppingList?.name ?? 'my list',
|
|
847
|
+
shoppingListId: shoppingList?.shoppingListId,
|
|
848
|
+
shoppingItems: items.map((i) => i.data.name),
|
|
849
|
+
});
|
|
850
|
+
}}
|
|
851
|
+
visibleModal={visibleModal}
|
|
852
|
+
setVisibleModal={setVisibleModal}
|
|
853
|
+
/>
|
|
854
|
+
<Container>
|
|
855
|
+
{!shoppingList ? (
|
|
856
|
+
<Spinner />
|
|
857
|
+
) : (
|
|
858
|
+
<Board<ListItem>
|
|
859
|
+
onItemsChange={(event) => {
|
|
860
|
+
const items = event.detail.items as BoardProps.Item<ListItem>[];
|
|
861
|
+
setShoppingItems(items);
|
|
862
|
+
putShoppingList.mutate({
|
|
863
|
+
name: shoppingList.name,
|
|
864
|
+
shoppingListId: shoppingList.shoppingListId,
|
|
865
|
+
shoppingItems: items.map((i) => i.data.name),
|
|
866
|
+
});
|
|
867
|
+
}}
|
|
868
|
+
items={shoppingItems || []}
|
|
869
|
+
renderItem={(item, actions) => (
|
|
870
|
+
<BoardItem
|
|
871
|
+
header={item.data.name}
|
|
872
|
+
settings={
|
|
873
|
+
<Button
|
|
874
|
+
iconName="close"
|
|
875
|
+
variant="icon"
|
|
876
|
+
onClick={actions.removeItem}
|
|
877
|
+
/>
|
|
878
|
+
}
|
|
879
|
+
i18nStrings={{
|
|
880
|
+
dragHandleAriaLabel: "Drag handle",
|
|
881
|
+
dragHandleAriaDescription:
|
|
882
|
+
"Use Space or Enter to activate drag, arrow keys to move, Space or Enter to submit, or Escape to discard.",
|
|
883
|
+
resizeHandleAriaLabel: "Resize handle",
|
|
884
|
+
resizeHandleAriaDescription:
|
|
885
|
+
"Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard.",
|
|
886
|
+
}}
|
|
887
|
+
/>
|
|
888
|
+
)}
|
|
889
|
+
i18nStrings={{
|
|
890
|
+
liveAnnouncementDndCommitted: () => "",
|
|
891
|
+
liveAnnouncementDndDiscarded: () => "",
|
|
892
|
+
liveAnnouncementDndItemInserted: () => "",
|
|
893
|
+
liveAnnouncementDndItemReordered: () => "",
|
|
894
|
+
liveAnnouncementDndItemResized: () => "",
|
|
895
|
+
liveAnnouncementDndStarted: () => "",
|
|
896
|
+
liveAnnouncementItemRemoved: () => "",
|
|
897
|
+
navigationAriaLabel: "",
|
|
898
|
+
navigationItemAriaLabel: () => "",
|
|
899
|
+
}}
|
|
900
|
+
empty={<></>}
|
|
901
|
+
/>
|
|
902
|
+
)}
|
|
903
|
+
</Container>
|
|
904
|
+
</ContentLayout>
|
|
905
|
+
);
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
export const Route = createFileRoute('/$shoppingListId')({
|
|
909
|
+
component: ShoppingList,
|
|
910
|
+
});
|
|
911
|
+
```
|
|
912
|
+
</TabItem>
|
|
913
|
+
</Tabs>
|
|
914
|
+
|
|
915
|
+
</Drawer>
|