From 4552263192e11213fb2d56d01c239dad89e990d1 Mon Sep 17 00:00:00 2001 From: Jacob Logan Date: Thu, 23 May 2024 03:25:56 -0700 Subject: [PATCH 1/2] Throw error when BlockSwitcher only has a single block --- .../BlockSwitcher/BlockSwitcher.tsx | 24 ++++++++++++------- .../__tests__/BlockSwitcher.test.tsx | 10 ++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/components/BlockSwitcher/BlockSwitcher.tsx b/src/components/BlockSwitcher/BlockSwitcher.tsx index 360e68e3b97..3509caa567b 100644 --- a/src/components/BlockSwitcher/BlockSwitcher.tsx +++ b/src/components/BlockSwitcher/BlockSwitcher.tsx @@ -2,20 +2,26 @@ import { Children } from 'react'; import { View, Tabs } from '@aws-amplify/ui-react'; import { BlockProps } from './Block'; -interface BlockSwitcher { - children: BlockProps | BlockProps[]; +interface BlockSwitcherProps { + children: React.ReactElement[]; } -export const BlockSwitcher = ({ children }) => { +export const BlockSwitcherErrorMessage = + 'BlockSwitcher requires more than one block element'; + +export const BlockSwitcher = ({ children }: BlockSwitcherProps) => { + if (!children.length || children.length <= 1) { + throw new Error(BlockSwitcherErrorMessage); + } return ( - + {Children.map(children, (child, index) => { return ( - child?.props?.name && ( - - {child?.props?.name} + child.props.name && ( + + {child.props.name} ) ); @@ -23,8 +29,8 @@ export const BlockSwitcher = ({ children }) => { {Children.map(children, (child, index) => { return ( - child?.props?.name && ( - + child.props.name && ( + {child} ) diff --git a/src/components/BlockSwitcher/__tests__/BlockSwitcher.test.tsx b/src/components/BlockSwitcher/__tests__/BlockSwitcher.test.tsx index eae43ec719c..8fdf3206208 100644 --- a/src/components/BlockSwitcher/__tests__/BlockSwitcher.test.tsx +++ b/src/components/BlockSwitcher/__tests__/BlockSwitcher.test.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { BlockSwitcherErrorMessage } from '../BlockSwitcher'; import { BlockSwitcher } from '../index'; import { Block } from '../index'; @@ -82,4 +83,13 @@ describe('BlockSwitcher', () => { expect(panels[2]).toHaveClass('amplify-tabs__panel--active'); }); }); + + it('should throw an error if only a single block exists', () => { + const singleBlock = ( + + {blockAContent} + + ); + expect(() => render(singleBlock)).toThrow(BlockSwitcherErrorMessage); + }); }); From a9c5d05d35ed6070585d95dbd337b7d2c4e0d4aa Mon Sep 17 00:00:00 2001 From: Jacob Logan Date: Fri, 24 May 2024 08:47:20 -0700 Subject: [PATCH 2/2] fix single block blockSwitchers --- .../ios/getting_started/30_install_lib.mdx | 8 +- .../lib/graphqlapi/ios/optimistic-ui.mdx | 32 ------- .../lib/graphqlapi/ios/working-with-files.mdx | 84 ------------------- .../common/setup_logging/setup_logging.mdx | 2 - .../logging/set-up-logging/index.mdx | 4 - .../lazy-load-custom-selection-set/index.mdx | 71 ---------------- .../cli/migration/list-nullability/index.mdx | 16 ---- 7 files changed, 1 insertion(+), 216 deletions(-) diff --git a/src/fragments/lib/geo/ios/getting_started/30_install_lib.mdx b/src/fragments/lib/geo/ios/getting_started/30_install_lib.mdx index 05fcd35f29f..a9f204eb3ce 100644 --- a/src/fragments/lib/geo/ios/getting_started/30_install_lib.mdx +++ b/src/fragments/lib/geo/ios/getting_started/30_install_lib.mdx @@ -1,8 +1,6 @@ The Geo plugin is dependent on Cognito Auth. - - - +#### Swift Package Manager 1. To install Amplify Geo and Authentication to your application, open your project in Xcode and select **File > Add Packages...**. @@ -11,7 +9,3 @@ The Geo plugin is dependent on Cognito Auth. 1. Choose the dependency rule **Up to Next Major Version**, as it will use the latest compatible version of the dependency, then click **Add Package**. 1. Lastly, choose **AWSLocationGeoPlugin**, **AWSCognitoAuthPlugin**, and **Amplify**. Then click Finish. - - - - \ No newline at end of file diff --git a/src/fragments/lib/graphqlapi/ios/optimistic-ui.mdx b/src/fragments/lib/graphqlapi/ios/optimistic-ui.mdx index 239560c9504..f1421b73abf 100644 --- a/src/fragments/lib/graphqlapi/ios/optimistic-ui.mdx +++ b/src/fragments/lib/graphqlapi/ios/optimistic-ui.mdx @@ -66,9 +66,6 @@ By providing these methods through an actor object, the underlying list will be To create an actor object that allows optimistic UI updates, create a new file and add the following code. - - - ```swift import Amplify import SwiftUI @@ -98,14 +95,8 @@ actor RealEstatePropertyList { } ``` - - - Calling the `listProperties()` method will perform a query with GraphQL API and store the results in the `properties` property. When this property is set, the list is sent back to the subscribers. In your UI, create a view model and subscribe to updates: - - - ```swift class RealEstatePropertyContainerViewModel: ObservableObject { @Published var properties: [RealEstateProperty] = [] @@ -140,16 +131,10 @@ struct RealEstatePropertyContainerView: View { } ``` - - - ## Optimistically rendering a newly created record To optimistically render a newly created record returned from the GraphQL API, add a method to the `actor RealEstatePropertyList`: - - - ```swift func createProperty(name: String, address: String? = nil) { let property = RealEstateProperty(name: name, address: address) @@ -179,16 +164,10 @@ func createProperty(name: String, address: String? = nil) { } ``` - - - ## Optimistically rendering a record update To optimistically render updates on a single item, use the code snippet like below: - - - ```swift func updateProperty(_ property: RealEstateProperty) async { guard let index = properties.firstIndex(where: { $0?.id == property.id }) else { @@ -215,17 +194,10 @@ func updateProperty(_ property: RealEstateProperty) async { } ``` - - - - ## Optimistically render deleting a record To optimistically render a GraphQL API delete, use the code snippet like below: - - - ```swift func deleteProperty(_ property: RealEstateProperty) async { guard let index = properties.firstIndex(where: { $0?.id == property.id }) else { @@ -258,10 +230,6 @@ func deleteProperty(_ property: RealEstateProperty) async { } ``` - - - - ## Complete example diff --git a/src/fragments/lib/graphqlapi/ios/working-with-files.mdx b/src/fragments/lib/graphqlapi/ios/working-with-files.mdx index 6a07a68e507..a1bccbfd29d 100644 --- a/src/fragments/lib/graphqlapi/ios/working-with-files.mdx +++ b/src/fragments/lib/graphqlapi/ios/working-with-files.mdx @@ -90,9 +90,6 @@ Make sure that the file key used for Storage is unique. In this case, the API re - - - ```swift let song = Song(name: name) @@ -122,16 +119,10 @@ guard case .success(let updatedSong) = updateResult else { } ``` - - - ## Add or update a file for an associated record To associate a new or different file with the record, update the existing record with the file key. The following example uploads the file using Storage and updates the record with the file's key. If an image is already associated with the record, this will update the record with the new image. - - - ```swift // Upload the new art image _ = try await Amplify.Storage.uploadData(key: currentSong.id, @@ -147,16 +138,10 @@ guard case .success(let updatedSong) = result else { } ``` - - - ## Query a record and retrieve the associated file To retrieve the file associated with a record, first query the record, then use Storage to download the data to display an image: - - - ```swift // Get the song record let result = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)) @@ -180,9 +165,6 @@ let imageData = try await Amplify.Storage.downloadData(key: coverArtKey, let image = UIImage(data: imageData) ``` - - - ## Delete and remove files associated with API records There are three common deletion workflows when working with Storage files and the GraphQL API: @@ -195,9 +177,6 @@ There are three common deletion workflows when working with Storage files and th The following example removes the file association from the record, but does not delete the file from S3 or the record from the DynamoDB instance. - - - ```swift // Get the song record let result = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)) @@ -223,16 +202,10 @@ guard case .success(let updatedSong) = updateResult else { } ``` - - - ### Remove the file association and delete the file The following example removes the file from the record, then deletes the file from S3: - - - ```swift // Get the song record let result = try await Amplify.API.query(request: .get(Song.self, byIdentifier: currentSong.id)) @@ -262,16 +235,10 @@ try await Amplify.Storage.remove(key: coverArtKey, options: .init(accessLevel: .private)) ``` - - - ### Delete both file and record The following example deletes the record from DynamoDB and then deletes the file from S3: - - - ```swift // Get the song record let result = try await Amplify.API.mutate(request: .get(Song.self, byIdentifier: currentSong.id)) @@ -298,9 +265,6 @@ guard case .success = deleteResult else { } ``` - - - ## Working with multiple files You may want to add multiple files to a single record, such as a user profile with multiple images. To do this, you can add a list of file keys to the record. The following example adds a list of file keys to a record: @@ -323,9 +287,6 @@ CRUD operations when working with multiple files is the same as when working wit First create a record via the GraphQL API, then upload the files to Storage, and finally add the associations between the record and files. - - - ```swift // Create the photo album record let album = PhotoAlbum(name: name) @@ -368,16 +329,10 @@ guard case .success(let updatedAlbum) = updateResult else { } ``` - - - ### Create a record with a single associated file When a schema allows for multiple associated images, you can still create a record with a single associated file. - - - ```swift // Create the photo album record let album = PhotoAlbum(name: name) @@ -402,16 +357,10 @@ guard case .success(let updatedAlbum) = updateResult else { } ``` - - - ### Add new files to an associated record To associate additional files with a record, update the record with the keys returned by the Storage uploads. - - - ```swift // Upload the new photo album image let key = "\(currentAlbum.id)-\(UUID().uuidString)" @@ -447,16 +396,10 @@ guard case .success(let updatedAlbum) = updateResult else { } ``` - - - ### Update the file for an associated record Updating a file for an associated record is the same as updating a file for a single file record, with the exception that you will need to update the list of file keys. The following replaces the last image in the album with a new image. - - - ```swift // Upload the new photo album image let key = "\(currentAlbum.id)-\(UUID().uuidString)" @@ -480,16 +423,10 @@ guard case .success(let updatedAlbum) = updateResult else { } ``` - - - ### Query a record and retrieve the associated files To retrieve the files associated with a record, first query the record, then use Storage to retrieve all the images. - - - ```swift // Get the song record let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) @@ -531,18 +468,12 @@ let images = await withTaskGroup(of: UIImage?.self) { group in } ``` - - - ### Delete and remove files associated with API records The workflow for deleting and removing files associated with API records is the same as when working with a single file, except that when performing a delete you will need to iterate over the list of files keys and call `Storage.remove()` for each file. #### Remove the file association, keep the persisted file and record - - - ```swift // Get the album record let result = try await Amplify.API.mutate(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) @@ -568,14 +499,8 @@ guard case .success(let updatedAlbum) = updateResult else { } ``` - - - #### Remove the file association and delete the files - - - ```swift // Get the album record let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) @@ -621,14 +546,8 @@ await withTaskGroup(of: Void.self) { group in } ``` - - - #### Delete the record and all associated files - - - ```swift // Get the album record let result = try await Amplify.API.query(request: .get(PhotoAlbum.self, byIdentifier: currentAlbum.id)) @@ -681,9 +600,6 @@ guard case .success = deleteResult else { } ``` - - - ## Data consistency when working with records and files The access patterns in this guide attempt to remove deleted files, but favor leaving orphans over leaving records that point to non-existent files. This optimizes for read latency by ensuring clients _rarely_ attempt to fetch a non-existent file from Storage. However, any app that deletes files can inherently cause records _on-device_ to point to non-existent files. diff --git a/src/fragments/lib/logging/common/setup_logging/setup_logging.mdx b/src/fragments/lib/logging/common/setup_logging/setup_logging.mdx index a9543b42a49..8309d7cdb0e 100644 --- a/src/fragments/lib/logging/common/setup_logging/setup_logging.mdx +++ b/src/fragments/lib/logging/common/setup_logging/setup_logging.mdx @@ -125,8 +125,6 @@ import androidInitWithoutConfig from '/src/fragments/lib/logging/android/setup_l }} /> - -{' '} - -{' '} To use the Amplify Logger and Amplify Auth categories in your app, you need to create and configure their corresponding plugins by calling the `Amplify.addPlugin()` and `Amplify.configure()` methods. @@ -495,8 +493,6 @@ init() { - -{' '} To use the Amplify Logger and Amplify Auth categories in your app, you need to create and configure their corresponding plugins by calling the `Amplify.add(plugin:)` and `Amplify.configure()` methods. diff --git a/src/pages/gen1/[platform]/tools/cli/migration/lazy-load-custom-selection-set/index.mdx b/src/pages/gen1/[platform]/tools/cli/migration/lazy-load-custom-selection-set/index.mdx index 621f091667f..e724a88efc3 100644 --- a/src/pages/gen1/[platform]/tools/cli/migration/lazy-load-custom-selection-set/index.mdx +++ b/src/pages/gen1/[platform]/tools/cli/migration/lazy-load-custom-selection-set/index.mdx @@ -65,9 +65,6 @@ type Comment @model @auth(rules: [{ allow: public }]) { Currently, developers querying for the `Comment` will contain the `Post` eager loaded: - - - ```swift let response = try await Amplify.API.query(request: .get(Comment.self, byId: "commentId")) if case .success(let queriedComment) = response { @@ -75,14 +72,8 @@ if case .success(let queriedComment) = response { } ``` - - - With the new model types and library changes, the same request will no longer eager load the post. The post is lazy loaded from the GraphQL service at the time the post is accessed. - - - ```swift let response = try await Amplify.API.query(request: .get(Comment.self, byId: "commentId")) if case .success(let queriedComment) = response { @@ -94,14 +85,8 @@ if case .success(let queriedComment) = response { } ``` - - - To achieve the previous behavior, specifying the model path using the new `includes` parameter: - - - ```swift let response = try await Amplify.API.query(request: .get(Comment.self, @@ -118,14 +103,8 @@ if case .success(let queriedComment) = response { This will populate the selection set of the post in the GraphQL document which indicates to your GraphQL service to retrieve the post model as part of the operation. Once you await on the post, the post model will immediately be returned without making a network request. - - - This customization extends to `@hasMany` relationships as well. Let's take for example, the queried post. - - - ```swift let response = try await Amplify.API.query(request: .get(Post.self, @@ -145,14 +124,8 @@ if case .success(let queriedPost) = response { The queried post allows you to lazy load the comments by calling `fetch()` and it will make a network request. - - - The comments can be eager loaded by including the post’s model path to the comment: - - - ```swift let response = try await Amplify.API.query(request: .get(Post.self, @@ -171,14 +144,8 @@ if case .success(let queriedPost) = response { The network request for post includes the comments, eager loading the comments in a single network call. - - - This customization can be extended to including or excluding deeply connected models. If the Post and Comment each belong to a User specified by the field “author”, then a single request can be constructed to retrieve its nested models. - - - ```swift let response = try await Amplify.API.query(request: .get( Post.self, @@ -191,32 +158,20 @@ let response = try await Amplify.API.query(request: .get( The post, its comments, and the author of the post and each of its comments will be retrieved in a single network call. - - - ## Lazy loading connected models Whether you are using **DataStore** or **API**, once you have retrieved a model, you can traverse the model graph from a single model instance to its connected models through the APIs available. For `@hasOne` and `@belongsTo` relations, access it by awaiting for the post. This will retrieve the model from your GraphQL service or local database in DataStore. - - - ```swift let comment = /* queried from Amplify.API or Amplify.DataStore, or lazy loaded from a post */ let post = try await comment.post let authorOfPost = try await post.author ``` - - - For `@hasMany` relations, call `fetch()` to load the posts. This will retrieve the list of models from your data source. - - - ```swift let post = /* queried from Amplify.API or Amplify.DataStore, or lazy loaded from a comment */ if let allCommentsForPost = post.comments { @@ -235,14 +190,8 @@ if allCommentsForPost.hasNextPage() { } ``` - - - The following is a full example of lazy loading `@belongsTo` and `@hasMany` connected models. - - - ```swift let comment = /* queried from Amplify.API or Amplify.DataStore, or lazy loaded from a post */ guard let post = try await comment.post else { @@ -266,9 +215,6 @@ if let allCommentsForPost = post.comments { The queried comment is used to lazy load its post. The author of the post is lazy loaded from the post. All of the comments for the post are lazy loaded as `allCommentsForPost`. For each comment, the author is loaded as `commentAuthor`. If there are more comments to load, `hasNextPage()` returns true and `getNextPage()` loads the next page from the underlying data source. - - - ## Cross platform app development with DataStore (Swift) Developers building with **DataStore (Swift)** can now receive real-time model updates coming from other platforms such as Amplify Studio and Amplify JavaScript and Android libraries. Previously, model updates (save/update/deletes) from other platforms will not be observed successfully by your iOS/macOS app running **DataStore (Swift)**. With the latest codegen and library changes, DataStore has been updated to successfully reconcile those model updates coming from other platforms, and will subsequently emit the event to your `DataStore.observe` API. @@ -323,9 +269,6 @@ By explicitly enabling the feature flag `generateModelsForLazyLoadAndCustomSelec **Amplify.API** will no longer eager load the `@belongsTo` and `@hasOne` connected models when using the latest codegen. To allow your app backwards compatibility with previous versions of your app, specify the model path with `includes` for all `@belongsTo` and `@hasOne` relationships. This is crucial to allow previous versions of the app to decode mutations sourced from new versions of the app successfully. - - - Your released app makes subscription and mutation requests: ```swift @@ -338,30 +281,18 @@ let graphQLResponse = try await Amplify.API.mutate(request: .create(comment)) The selection set on the mutation request is aligned with the selection set on the subscription request. It will include the post fields and the response payload received by the subscription can be decoded to the previous Comment model type. - - - If the model types have been replaced with the latest codegen for lazy loading, the same mutation will no longer include the post fields, causing the subscription in the previous app to fail decoding the response payload. To make sure the new version of the app works with previous versions, include the `@belongsTo` and `@hasOne` connected models in the selection set using the `includes` parameter of your mutation request. - - - ```swift let graphQLResponse = try await Amplify.API.mutate(request: .create(comment, includes: { comment in [ comment.post ]})) ``` - - - ### Scenario 2. Using DataStore (Swift) DataStore will no longer eager load the belongs-to and `@hasOne` connected models when using the latest codegen. Your new app will continue to be backwards compatible with previous versions, however the call pattern to retrieve these connected models have changed. See the next scenario for the changes you have to make at the call site. ### Scenario 3. "Belongs to" / "Has One" access pattern - - - Previously ```swift @@ -376,5 +307,3 @@ let comment = /* queried Comment through DataStore or API */ let post = try await comment.post ``` - - diff --git a/src/pages/gen1/[platform]/tools/cli/migration/list-nullability/index.mdx b/src/pages/gen1/[platform]/tools/cli/migration/list-nullability/index.mdx index 3292c2d78ba..60d16f2c934 100644 --- a/src/pages/gen1/[platform]/tools/cli/migration/list-nullability/index.mdx +++ b/src/pages/gen1/[platform]/tools/cli/migration/list-nullability/index.mdx @@ -59,10 +59,6 @@ This is to align the optionality of the generated Swift models as closely as pos ### **Who is impacted?** - - - - Developers building an iOS app with Amplify DataStore or Amplify API generates Swift models by running the command `amplify codegen models`. _Previous generated Swift code_ @@ -96,10 +92,6 @@ The difference between the current and previous code: - `optionalElementRequiredList` - the list component was required and is now optional. The list was optional and is now required - `optionalElementOptionalList` - the list component was required and is now optional. - - - - ### **When do I have to upgrade?** This is behind a feature flag in Amplify CLI 5.1.2 and will be deprecated by November 1st, 2021. Developers with existing apps should upgrade to the latest CLI, set the feature flag, and update their app code or their schema (see recommendations following) to account for the change in optionality of the types. Developers building a new app will automatically generate code with the latest changes and no action is required. @@ -124,10 +116,6 @@ amplify --v # at least 5.1.2 5. Open the App and make sure the app compiles with the latest generated models. Depending on your schema, you may be in the following scenarios. - - - - Scenario 1. Schema: `requiredElementOptionalList: [String!]` ```swift @@ -211,7 +199,3 @@ if let optionalElementList = container.optionalElementOptionalList { Since the list component was required and is now optional, unwrap the optional value to retrieve the value. **Recommendation:** Update the type in the schema from `[String]` to `[String!]!` to make the list and list component required if you do not store null values in the list or a null list. This will remove the need to unwrap the list and the list components. - - - -