diff --git a/.github/workflows/check_for_broken_links.yml b/.github/workflows/check_for_broken_links.yml index f03377ccafc..181c6a1458b 100644 --- a/.github/workflows/check_for_broken_links.yml +++ b/.github/workflows/check_for_broken_links.yml @@ -32,7 +32,7 @@ jobs: role-to-assume: arn:aws:iam::464149486631:role/github_action_read_slack_webhook_url aws-region: us-west-2 - name: Read secrets from AWS Secrets Manager into environment variables - uses: aws-actions/aws-secretsmanager-get-secrets@ff26a0aa6bd4dd5e51326b5afb3f5f6874c958c7 # v2.0.3 + uses: aws-actions/aws-secretsmanager-get-secrets@98c2d6bf1dd67c2575fa2bb14294aa64103d426c # v2.0.5 with: secret-ids: | SLACK_WEBHOOK_URL diff --git a/.github/workflows/scripts/validate-redirects.js b/.github/workflows/scripts/validate-redirects.js new file mode 100644 index 00000000000..de89c1e3802 --- /dev/null +++ b/.github/workflows/scripts/validate-redirects.js @@ -0,0 +1,58 @@ +module.exports = { + invalidRedirects: () => { + const Ajv = require('ajv'); + const redirects = require('../../../redirects.json'); + const ajv = new Ajv(); + + const schema = { + type: 'array', + items: { + type: 'object', + required: ['source', 'target', 'status'], + properties: { + source: { + description: 'The address the user requested.', + type: 'string', + pattern: '^/' + }, + target: { + description: + 'The address that actually serves the content that the user sees', + type: 'string', + pattern: '^[(https)(/)]' + }, + status: { + description: + 'Types include a permanent redirect (301), a temporary redirect (302), a rewrite (200), or not found (404).', + type: 'string', + pattern: '^[0-5-]+$' + } + } + } + }; + + const errors = []; + const validate = ajv.compile(schema); + + const validateEntries = (redirects) => { + const valid = validate(redirects); + + if (!valid) { + const error = validate.errors[0]; + const invalidEntry = + JSON.stringify(redirects[error.instancePath.slice(1, -7)]); + const loc = error.schemaPath.slice(error.schemaPath.indexOf('properties') + 11, -8); + const errorMessage = '\n\n' + 'INVALID ENTRY: Please correct the error in the "' + loc +'" property of the following entry: \n' + invalidEntry + '\n' + 'ERROR MESSAGE: ' + error.message; + errors.push(errorMessage); + + validateEntries(redirects.splice(parseInt(error.instancePath.slice(1, -7)) + 1)); + + } + } + validateEntries(redirects); + + return errors; + } +} + + diff --git a/.github/workflows/validate_redirects.yml b/.github/workflows/validate_redirects.yml new file mode 100644 index 00000000000..e37598b26c2 --- /dev/null +++ b/.github/workflows/validate_redirects.yml @@ -0,0 +1,32 @@ +name: Validate Redirects +on: + pull_request: + branches: [main] + types: [opened, synchronize] +env: + BUILD_DIR: 'client/www/next-build' +permissions: + contents: read +jobs: + ValidateRedirects: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Setup Node.js 20.x + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: 20.x + - name: Install Dependencies + run: yarn + - name: Validate redirects + id: redirects + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + result-encoding: string + script: | + const { invalidRedirects } = require('./.github/workflows/scripts/validate-redirects.js'); + return await invalidRedirects(); + - name: Fail if any invalid redirects have been found + if: ${{ steps.redirects.outputs.result }} + run: exit 1 && echo ${{ steps.redirects.outputs.result }} diff --git a/cspell.json b/cspell.json index 7627a76a2b7..33fc3d5d2d9 100644 --- a/cspell.json +++ b/cspell.json @@ -532,10 +532,12 @@ "Didfinishlaunchingwithoptions", "displayMode", "displayOrder", + "dists", "DocSet", "DocSets", "Donef", "Dont", + "dotenvx", "downcasting", "dropdown", "dynamoDB", diff --git a/package.json b/package.json index 492baeac11c..0861e00620d 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,10 @@ "license": "Apache-2.0", "private": true, "dependencies": { - "@aws-amplify/amplify-cli-core": "^4.3.8", + "@aws-amplify/amplify-cli-core": "^4.3.9", "@aws-amplify/ui-react": "^6.1.12", "@docsearch/react": "3", + "ajv": "^8.16.0", "aws-amplify": "^6.0.9", "next": "^14.2.3", "next-image-export-optimizer": "^1.8.3", diff --git a/redirects.json b/redirects.json index 9d74a7b3294..1655fdaae20 100644 --- a/redirects.json +++ b/redirects.json @@ -3011,7 +3011,7 @@ }, { "source": "/lib/client-configuration/configuring-amplify-categories/q/platform/react-native/", - "target": "react-native/tools/libraries/configure-categories/", + "target": "/react-native/tools/libraries/configure-categories/", "status": "301" }, { diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx index f4729dcbfa0..097e94481a0 100644 --- a/src/components/Accordion/Accordion.tsx +++ b/src/components/Accordion/Accordion.tsx @@ -69,10 +69,12 @@ export const Accordion: React.FC = ({ const closeAccordion = () => { const details = detailsRef.current; + const summary = summaryRef.current; if (details) { const scrollToLoc = details.offsetTop - 48 - 70 - 10; // account for nav heights and 10px buffer setDetailsOpen(false); details.animate(collapse, animationTiming); + summary?.focus(); window.scrollTo({ left: 0, top: scrollToLoc, diff --git a/src/components/Accordion/__tests__/Accordion.test.tsx b/src/components/Accordion/__tests__/Accordion.test.tsx index 1b8f6141e98..9967742f0f0 100644 --- a/src/components/Accordion/__tests__/Accordion.test.tsx +++ b/src/components/Accordion/__tests__/Accordion.test.tsx @@ -65,11 +65,13 @@ describe('Accordion', () => { }); }); - it('should collapse Accordion when close button is clicked', async () => { + it('should collapse Accordion and refocus on Accordion element when close button is clicked', async () => { render(component); const accordionHeading = screen.getByText('Accordion component example'); userEvent.click(accordionHeading); const detailsEl = await screen.getByRole('group'); + const summaryEl = detailsEl.firstChild; + expect(detailsEl).toHaveAttribute('open'); const text = await screen.getByText(content); @@ -79,6 +81,7 @@ describe('Accordion', () => { await waitFor(() => { expect(text).not.toBeVisible(); expect(detailsEl).not.toHaveAttribute('open'); + expect(summaryEl).toHaveFocus(); }); }); diff --git a/src/components/Callout/Callout.tsx b/src/components/Callout/Callout.tsx index 0d40770c0e2..b1b785ea002 100644 --- a/src/components/Callout/Callout.tsx +++ b/src/components/Callout/Callout.tsx @@ -3,12 +3,21 @@ import { Message, View } from '@aws-amplify/ui-react'; interface CalloutProps { info?: boolean; warning?: boolean; + backgroundColor?: string; children?: React.ReactNode; } -export const Callout = ({ warning, children }: CalloutProps) => { +export const Callout = ({ + warning, + backgroundColor, + children +}: CalloutProps) => { return ( - + {children} ); diff --git a/src/components/Callout/__tests__/Callout.test.tsx b/src/components/Callout/__tests__/Callout.test.tsx index 260069dc805..6b5b32f5d73 100644 --- a/src/components/Callout/__tests__/Callout.test.tsx +++ b/src/components/Callout/__tests__/Callout.test.tsx @@ -19,4 +19,17 @@ describe('Callout', () => { consoleErrorFn.mockRestore(); }); + + it('should pass the backgroundColor through to the Message component', async () => { + const child =
Callout Child
; + const ele = render( + + {child} + + ); + + const styles = getComputedStyle(ele.container.children[0]); + console.log(styles); + expect(styles.backgroundColor).toBe('red'); + }); }); diff --git a/src/components/FeatureFlags/feature-flags.json b/src/components/FeatureFlags/feature-flags.json index a2e95d99bf9..b4ccaf9c1b7 100644 --- a/src/components/FeatureFlags/feature-flags.json +++ b/src/components/FeatureFlags/feature-flags.json @@ -371,6 +371,26 @@ "defaultExistingProject": false } ] + }, + "subscriptionsInheritPrimaryAuth": { + "description": "Toggles whether subscriptions will inherit related authorization when relational fields are set as required", + "type": "Feature", + "valueType": "Boolean", + "versionAdded": "12.12.4", + "values": [ + { + "value": "true", + "description": "Subscriptions will inherit the primary model authorization rules for the relational fields", + "defaultNewProject": false, + "defaultExistingProject": false + }, + { + "value": "false", + "description": "Relational fields will be redacted in mutation response when there is a difference between auth rules between primary and related models.", + "defaultNewProject": true, + "defaultExistingProject": true + } + ] } } }, diff --git a/src/components/Feedback/index.tsx b/src/components/Feedback/index.tsx index 1ee95e4cf0a..53906ac1d8d 100644 --- a/src/components/Feedback/index.tsx +++ b/src/components/Feedback/index.tsx @@ -189,7 +189,12 @@ const Feedback = function Feedback(router) { return ( -
+
{c.feedbackQuestion} diff --git a/src/components/Gen1Banner/Gen1Banner.tsx b/src/components/Gen1Banner/Gen1Banner.tsx new file mode 100644 index 00000000000..c39fff78707 --- /dev/null +++ b/src/components/Gen1Banner/Gen1Banner.tsx @@ -0,0 +1,20 @@ +import { Callout } from '@/components/Callout'; +import Link from 'next/link'; +import classNames from 'classnames'; + +export const Gen1Banner = ({ currentPlatform }) => { + return ( + + For new Amplify apps, we recommend using Amplify Gen 2. You can learn more + in our{' '} + + Gen 2 Docs + + . + + ); +}; diff --git a/src/components/Gen1Banner/index.ts b/src/components/Gen1Banner/index.ts new file mode 100644 index 00000000000..36949bf086f --- /dev/null +++ b/src/components/Gen1Banner/index.ts @@ -0,0 +1 @@ +export { Gen1Banner } from './Gen1Banner'; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index b4404b201aa..4b73cc128cc 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -35,6 +35,7 @@ import { NEXT_PREVIOUS_SECTIONS } from '@/components/NextPrevious'; import { Modal } from '@/components/Modal'; +import { Gen1Banner } from '@/components/Gen1Banner'; export const Layout = ({ children, @@ -127,6 +128,13 @@ export const Layout = ({ } }, 20); + const isGen1GettingStarted = /\/gen1\/\w+\/start\/getting-started\//.test( + asPathWithNoHash + ); + const isGen1HowAmplifyWorks = /\/gen1\/\w+\/how-amplify-works\//.test( + asPathWithNoHash + ); + useEffect(() => { const headings: HeadingInterface[] = []; @@ -254,6 +262,9 @@ export const Layout = ({ {useCustomTitle ? null : ( {pageTitle} )} + {(isGen1GettingStarted || isGen1HowAmplifyWorks) && ( + + )} {children} {showNextPrev && } diff --git a/src/components/MDXComponents/MDXCopyCodeButton.tsx b/src/components/MDXComponents/MDXCopyCodeButton.tsx index ad76cf72a58..d3502af303d 100644 --- a/src/components/MDXComponents/MDXCopyCodeButton.tsx +++ b/src/components/MDXComponents/MDXCopyCodeButton.tsx @@ -33,10 +33,9 @@ export const MDXCopyCodeButton = ({