diff --git a/.env b/.env index e2508f7a05e6..57136408c153 100644 --- a/.env +++ b/.env @@ -35,3 +35,4 @@ SRNT_GOOGLE_BUCKET_NAME=bucket XDEBUG_MODE=off FLAG_FREE_TRIAL_ENABLED=0 FLAG_MARKETPLACE_ACTIVATE_ENABLED=0 +FLAG_CONNECT_APP_WITH_PERMISSIONS_ENABLED=0 diff --git a/config/packages/akeneo_feature_flag.yml b/config/packages/akeneo_feature_flag.yml index 0d5a3ace9837..b02debb2b5ec 100644 --- a/config/packages/akeneo_feature_flag.yml +++ b/config/packages/akeneo_feature_flag.yml @@ -4,3 +4,4 @@ akeneo_feature_flag: - { feature: 'data_quality_insights', service: 'akeneo.pim.automation.data_quality_insights.feature' } - { feature: 'free_trial', service: 'akeneo.free_trial.feature' } - { feature: 'marketplace_activate', service: 'akeneo_connectivity.connection.marketplace_activate.feature' } + - { feature: 'connect_app_with_permissions', service: 'akeneo_connectivity.connection.connect_app_with_permissions.feature' } diff --git a/src/Akeneo/Connectivity/Connection/back/Infrastructure/Symfony/Resources/config/feature_flag.yml b/src/Akeneo/Connectivity/Connection/back/Infrastructure/Symfony/Resources/config/feature_flag.yml index 075f259e20f0..38384bab8dc3 100644 --- a/src/Akeneo/Connectivity/Connection/back/Infrastructure/Symfony/Resources/config/feature_flag.yml +++ b/src/Akeneo/Connectivity/Connection/back/Infrastructure/Symfony/Resources/config/feature_flag.yml @@ -1,6 +1,16 @@ +parameters: + env(FLAG_MARKETPLACE_ACTIVATE_ENABLED): 0 + env(FLAG_CONNECT_APP_WITH_PERMISSIONS_ENABLED): 0 + services: akeneo_connectivity.connection.marketplace_activate.feature: class: Akeneo\Platform\Bundle\FeatureFlagBundle\Configuration\EnvVarFeatureFlag arguments: - '%env(bool:FLAG_MARKETPLACE_ACTIVATE_ENABLED)%' public: true + + akeneo_connectivity.connection.connect_app_with_permissions.feature: + class: Akeneo\Platform\Bundle\FeatureFlagBundle\Configuration\EnvVarFeatureFlag + arguments: + - '%env(bool:FLAG_CONNECT_APP_WITH_PERMISSIONS_ENABLED)%' + public: true diff --git a/src/Akeneo/Connectivity/Connection/back/Infrastructure/Symfony/Resources/translations/jsmessages.en_US.yml b/src/Akeneo/Connectivity/Connection/back/Infrastructure/Symfony/Resources/translations/jsmessages.en_US.yml index f089127ebe50..b985d5271ade 100644 --- a/src/Akeneo/Connectivity/Connection/back/Infrastructure/Symfony/Resources/translations/jsmessages.en_US.yml +++ b/src/Akeneo/Connectivity/Connection/back/Infrastructure/Symfony/Resources/translations/jsmessages.en_US.yml @@ -63,34 +63,43 @@ akeneo_connectivity.connection: unreachable: We can't reach the marketplace, please come back later. scroll_to_top: Scroll to the top apps: + wizard: + title: Connect + action: + confirm: Confirm + cancel: Cancel + previous: Previous + next: Next + allow_and_next: Allow and next + progress: + authorizations: Authorizations + permissions: Permissions + well_done: Well done! + authorize: + title: 'App {{ app_name }} needs to' + no_scope_title: 'The {{ app_name }} App would like to access your PIM.' + no_scope: No specific authorizations have been requested. + helper: To know more about about app authorization, + helper_link: check out our Help Center for more information. title: Connect - action: - confirm: Confirm - cancel: Cancel activate: flash: error: Sorry, an error occurred while connecting the App. - authorize: - title: 'App {{ app_name }} needs to' - no_scope_title: 'The {{ app_name }} App would like to access your PIM.' - no_scope: No specific authorizations have been requested. - helper: To know more about about app authorization, - helper_link: check out our Help Center for more information. - error: - sub_text: Please contact the app developer or Akeneo support - scope: - type: - view: 'View {{ entities }}' - edit: 'View and edit {{ entities }}' - delete: 'View, edit and delete {{ entities }}' - entities: - catalog_structure: attributes, attribute groups, families and family variants - attribute_options: attribute options - categories: categories - channel_localization: locales and currencies - channel_settings: channels - association_types: association types - products: products and product models + scope: + type: + view: 'View {{ entities }}' + edit: 'View and edit {{ entities }}' + delete: 'View, edit and delete {{ entities }}' + entities: + catalog_structure: attributes, attribute groups, families and family variants + attribute_options: attribute options + categories: categories + channel_localization: locales and currencies + channel_settings: channels + association_types: association types + products: products and product models + error: + sub_text: Please contact the app developer or Akeneo support constraint: client_id: not_blank: The client_id is required. diff --git a/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizard/AppWizard.tsx b/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizard/AppWizard.tsx index 84d5fe48b5e5..c82f1c18a641 100644 --- a/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizard/AppWizard.tsx +++ b/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizard/AppWizard.tsx @@ -2,9 +2,10 @@ import React, {FC, useEffect, useState} from 'react'; import styled from 'styled-components'; import {Button, getColor, getFontSize, Modal} from 'akeneo-design-system'; import {useHistory} from 'react-router'; -import {AppWizardData, useFetchAppWizardData} from '../../hooks/use-fetch-app-wizard-data'; +import {useFetchAppWizardData} from '../../hooks/use-fetch-app-wizard-data'; import {useTranslate} from '../../../shared/translate'; -import {ScopeList} from './ScopeList'; +import {AppWizardData} from '../../../model/Apps/wizard-data'; +import {ScopeListContainer} from './ScopeListContainer'; const Content = styled.div` display: grid; @@ -44,7 +45,7 @@ const Connect = styled.h3` font-size: ${getFontSize('default')}; text-transform: uppercase; font-weight: normal; - margin: 0; + margin: 0 0 6px 0; `; const ActionButton = styled(Button)` @@ -76,7 +77,7 @@ export const AppWizard: FC = ({clientId}) => { return ( @@ -86,13 +87,13 @@ export const AppWizard: FC = ({clientId}) => { {translate('akeneo_connectivity.connection.connect.apps.title')} - + - {translate('akeneo_connectivity.connection.connect.apps.action.cancel')} + {translate('akeneo_connectivity.connection.connect.apps.wizard.action.cancel')} - {translate('akeneo_connectivity.connection.connect.apps.action.confirm')} + {translate('akeneo_connectivity.connection.connect.apps.wizard.action.confirm')} diff --git a/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizard/ScopeList.tsx b/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizard/ScopeList.tsx deleted file mode 100644 index 925fff5e859e..000000000000 --- a/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizard/ScopeList.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React, {FC} from 'react'; -import styled from 'styled-components'; -import { - getColor, - getFontSize, - Link, - ProductIcon, - CheckRoundIcon, - AddAttributeIcon, - AssociateIcon, - ShopIcon, - CategoryIcon, - LocaleIcon, - GroupsIcon, -} from 'akeneo-design-system'; -import {useTranslate} from '../../../shared/translate'; -import {ScopeMessage} from '../../hooks/use-fetch-app-wizard-data'; - -const AppTitle = styled.h2` - color: ${getColor('grey', 140)}; - font-size: 28px; - font-weight: normal; - line-height: 28px; - margin: 0; -`; - -const Helper = styled.div` - color: ${getColor('grey', 120)}; - font-size: ${getFontSize('default')}; - font-weight: normal; - line-height: 18px; - margin: 10px 0 19px 0; - width: 280px; -`; - -const ScopeItem = styled.li` - color: ${getColor('grey', 140)}; - font-size: ${getFontSize('bigger')}; - font-weight: normal; - line-height: 21px; - margin-bottom: 13px; - display: flex; - align-items: center; - - & > svg { - margin-right: 10px; - color: ${getColor('grey', 100)}; - } -`; - -const iconsMap: {[key: string]: React.ElementType} = { - catalog_structure: GroupsIcon, - attribute_options: AddAttributeIcon, - categories: CategoryIcon, - channel_settings: ShopIcon, - channel_localization: LocaleIcon, - association_types: AssociateIcon, - products: ProductIcon, -}; - -interface Props { - appName: string; - scopeMessages: ScopeMessage[]; -} - -export const ScopeList: FC = ({appName, scopeMessages}) => { - const translate = useTranslate(); - - let scopeList = scopeMessages.map((scopeMessage, key) => { - const entities = translate( - `akeneo_connectivity.connection.connect.apps.authorize.scope.entities.${scopeMessage.entities}` - ); - const Icon = iconsMap[scopeMessage.icon] ?? CheckRoundIcon; - - return ( - - -
${entities}`} - ), - }} - /> - - ); - }); - - const title = - scopeList.length === 0 - ? translate('akeneo_connectivity.connection.connect.apps.authorize.no_scope_title', {app_name: appName}) - : translate('akeneo_connectivity.connection.connect.apps.authorize.title', {app_name: appName}); - - if (scopeList.length === 0) { - const message = translate('akeneo_connectivity.connection.connect.apps.authorize.no_scope'); - scopeList = [ - - - {message} - , - ]; - } - - return ( - <> - {title} - -

{translate('akeneo_connectivity.connection.connect.apps.authorize.helper')}

- - {translate('akeneo_connectivity.connection.connect.apps.authorize.helper_link')} - -
-
    {scopeList}
- - ); -}; diff --git a/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizard/ScopeListContainer.tsx b/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizard/ScopeListContainer.tsx new file mode 100644 index 000000000000..38836a6d1ea6 --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizard/ScopeListContainer.tsx @@ -0,0 +1,66 @@ +import React, {FC} from 'react'; +import styled from 'styled-components'; +import {getColor, getFontSize, Link, CheckRoundIcon} from 'akeneo-design-system'; +import {useTranslate} from '../../../shared/translate'; +import {ScopeItem, ScopeList} from '../ScopeList'; +import ScopeMessage from '../../../model/Apps/scope-message'; + +const AppTitle = styled.h2` + color: ${getColor('grey', 140)}; + font-size: 28px; + font-weight: normal; + line-height: 28px; + margin: 0; +`; + +const Helper = styled.div` + color: ${getColor('grey', 120)}; + font-size: ${getFontSize('default')}; + font-weight: normal; + line-height: 18px; + margin: 17px 0 19px 0; + width: 280px; +`; + +interface Props { + appName: string; + scopeMessages: ScopeMessage[]; +} + +export const ScopeListContainer: FC = ({appName, scopeMessages}) => { + const translate = useTranslate(); + + const title = + scopeMessages.length === 0 + ? translate('akeneo_connectivity.connection.connect.apps.wizard.authorize.no_scope_title', { + app_name: appName, + }) + : translate('akeneo_connectivity.connection.connect.apps.wizard.authorize.title', {app_name: appName}); + + return ( + <> + {title} + +

{translate('akeneo_connectivity.connection.connect.apps.wizard.authorize.helper')}

+ + {translate('akeneo_connectivity.connection.connect.apps.wizard.authorize.helper_link')} + +
+ {0 === scopeMessages.length ? ( + + + {translate('akeneo_connectivity.connection.connect.apps.wizard.authorize.no_scope')} + + ) : ( + + )} + + ); +}; diff --git a/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizardWithSteps/AppWizardWithSteps.tsx b/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizardWithSteps/AppWizardWithSteps.tsx new file mode 100644 index 000000000000..95ac54bc4f4e --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizardWithSteps/AppWizardWithSteps.tsx @@ -0,0 +1,100 @@ +import React, {FC, useEffect, useState} from 'react'; +import {useHistory} from 'react-router'; +import {Button, Modal, useProgress, ProgressIndicator} from 'akeneo-design-system'; +import styled from 'styled-components'; +import {Permissions} from './Permissions'; +import {Authorizations} from './Authorizations'; +import {AppWizardData} from '../../../model/Apps/wizard-data'; +import {useFetchAppWizardData} from '../../hooks/use-fetch-app-wizard-data'; +import {useTranslate} from '../../../shared/translate'; + +const Content = styled.div` + display: grid; + grid-template-columns: 260px 593px; + grid-template-areas: 'LOGO INFO'; +`; +const LogoContainer = styled.div` + grid-area: LOGO; + padding-right: 40px; +`; +const Logo = styled.img` + width: 220px; + height: 220px; +`; + +const AllowAndNextButton = styled(Button)` + position: fixed; + top: 40px; + right: 40px; +`; +const PreviousButton = styled(Button)` + position: fixed; + top: 40px; + left: 80px; +`; +const ProgressIndicatorContainer = styled(ProgressIndicator)` + width: 456px; + height: 70px; + position: fixed; + bottom: 20px; +`; + +interface Props { + clientId: string; +} + +export const AppWizardWithSteps: FC = ({clientId}) => { + const translate = useTranslate(); + const history = useHistory(); + const [wizardData, setWizardData] = useState(null); + const fetchWizardData = useFetchAppWizardData(clientId); + const steps: string[] = ['authorizations', 'permissions', 'well_done']; + const [isCurrent, next, previous] = useProgress(steps); + useEffect(() => { + fetchWizardData().then(setWizardData); + }, [fetchWizardData]); + + const redirectToMarketplace = () => { + history.push('/connect/marketplace'); + }; + + if (wizardData === null) { + return null; + } + + return ( + + {!isCurrent('authorizations') && ( + + {translate('akeneo_connectivity.connection.connect.apps.wizard.action.previous')} + + )} + + {isCurrent('authorizations') + ? translate('akeneo_connectivity.connection.connect.apps.wizard.action.allow_and_next') + : translate('akeneo_connectivity.connection.connect.apps.wizard.action.next')} + + + + + + + {isCurrent('authorizations') && ( + + )} + {isCurrent('permissions') && } + + + + {steps.map(step => ( + + {translate(`akeneo_connectivity.connection.connect.apps.wizard.progress.${step}`)} + + ))} + + + ); +}; diff --git a/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizardWithSteps/Authorizations.tsx b/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizardWithSteps/Authorizations.tsx new file mode 100644 index 000000000000..676be6fcf30a --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizardWithSteps/Authorizations.tsx @@ -0,0 +1,41 @@ +import React, {FC} from 'react'; +import styled from 'styled-components'; +import {getColor, getFontSize} from 'akeneo-design-system'; +import {useTranslate} from '../../../shared/translate'; +import {ScopeListContainer} from '../AppWizard/ScopeListContainer'; + +const InfoContainer = styled.div` + grid-area: INFO; + padding: 20px 0 20px 40px; + border-left: 1px solid ${getColor('brand', 100)}; +`; + +const Connect = styled.h3` + color: ${getColor('brand', 100)}; + font-size: ${getFontSize('default')}; + text-transform: uppercase; + font-weight: normal; + margin: 0; +`; + +type ScopeMessages = { + icon: string; + type: string; + entities: string; +}; + +type Props = { + appName: string; + scopeMessages: ScopeMessages[]; +}; + +export const Authorizations: FC = ({appName, scopeMessages}) => { + const translate = useTranslate(); + + return ( + + {translate('akeneo_connectivity.connection.connect.apps.title')} + + + ); +}; diff --git a/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizardWithSteps/Permissions.tsx b/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizardWithSteps/Permissions.tsx new file mode 100644 index 000000000000..dfadb1e7b977 --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/src/connect/components/AppWizardWithSteps/Permissions.tsx @@ -0,0 +1,5 @@ +import React, {FC} from 'react'; + +export const Permissions: FC = () => { + return
Hello permissions!
; +}; diff --git a/src/Akeneo/Connectivity/Connection/front/src/connect/components/ScopeList.tsx b/src/Akeneo/Connectivity/Connection/front/src/connect/components/ScopeList.tsx new file mode 100644 index 000000000000..b84a635f1140 --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/src/connect/components/ScopeList.tsx @@ -0,0 +1,78 @@ +import React, {FC} from 'react'; +import {useTranslate} from '../../shared/translate'; +import styled from 'styled-components'; +import { + AddAttributeIcon, + AssociateIcon, + CategoryIcon, + getColor, + getFontSize, + GroupsIcon, + LocaleIcon, + ProductIcon, + ShopIcon, + CheckRoundIcon, +} from 'akeneo-design-system'; +import ScopeMessage from '../../model/Apps/scope-message'; + +export const ScopeItem = styled.li` + color: ${getColor('grey', 140)}; + font-size: ${getFontSize('bigger')}; + font-weight: normal; + line-height: 21px; + margin-bottom: 13px; + display: flex; + align-items: center; + + & > svg { + margin-right: 10px; + color: ${getColor('grey', 100)}; + } +`; + +const iconsMap: {[key: string]: React.ElementType} = { + catalog_structure: GroupsIcon, + attribute_options: AddAttributeIcon, + categories: CategoryIcon, + channel_settings: ShopIcon, + channel_localization: LocaleIcon, + association_types: AssociateIcon, + products: ProductIcon, +}; + +interface Props { + scopeMessages: ScopeMessage[]; +} + +export const ScopeList: FC = ({scopeMessages}) => { + const translate = useTranslate(); + + return ( +
    + {scopeMessages.map((scopeMessage, key) => { + const entities = translate( + `akeneo_connectivity.connection.connect.apps.scope.entities.${scopeMessage.entities}` + ); + const Icon = iconsMap[scopeMessage.icon] ?? CheckRoundIcon; + + return ( + + +
    + ${entities} + `, + } + ), + }} + /> + + ); + })} +
+ ); +}; diff --git a/src/Akeneo/Connectivity/Connection/front/src/connect/hooks/use-fetch-app-wizard-data.ts b/src/Akeneo/Connectivity/Connection/front/src/connect/hooks/use-fetch-app-wizard-data.ts index 463101bc3cd8..622ed20cabed 100644 --- a/src/Akeneo/Connectivity/Connection/front/src/connect/hooks/use-fetch-app-wizard-data.ts +++ b/src/Akeneo/Connectivity/Connection/front/src/connect/hooks/use-fetch-app-wizard-data.ts @@ -1,18 +1,6 @@ import {useRoute} from '../../shared/router'; import {useCallback} from 'react'; -export interface ScopeMessage { - icon: string; - type: string; - entities: string; -} - -export interface AppWizardData { - appName: string; - appLogo: string; - scopeMessages: ScopeMessage[]; -} - export const useFetchAppWizardData = (clientId: string) => { const url = useRoute('akeneo_connectivity_connection_apps_rest_get_wizard_data', {clientId: clientId}); diff --git a/src/Akeneo/Connectivity/Connection/front/src/connect/pages/AppAuthorizePage.tsx b/src/Akeneo/Connectivity/Connection/front/src/connect/pages/AppAuthorizePage.tsx index d407659e1e7e..4209bd50aa00 100644 --- a/src/Akeneo/Connectivity/Connection/front/src/connect/pages/AppAuthorizePage.tsx +++ b/src/Akeneo/Connectivity/Connection/front/src/connect/pages/AppAuthorizePage.tsx @@ -3,6 +3,8 @@ import {useLocation} from 'react-router-dom'; import {AuthorizeClientError} from '../components/AuthorizeClientError'; import {AppWizard} from '../components/AppWizard/AppWizard'; import {useHistory} from 'react-router'; +import {AppWizardWithSteps} from '../components/AppWizardWithSteps/AppWizardWithSteps'; +import {useFeatureFlags} from '../../shared/feature-flags'; export const AppAuthorizePage: FC = () => { const history = useHistory(); @@ -10,6 +12,7 @@ export const AppAuthorizePage: FC = () => { const query = new URLSearchParams(location.search); const error = query.get('error'); const clientId = query.get('client_id'); + const featureFlags = useFeatureFlags(); if (null !== error) { return ; @@ -20,5 +23,9 @@ export const AppAuthorizePage: FC = () => { return null; } + if (true === featureFlags.isEnabled('connect_app_with_permissions')) { + return ; + } + return ; }; diff --git a/src/Akeneo/Connectivity/Connection/front/src/model/Apps/scope-message.ts b/src/Akeneo/Connectivity/Connection/front/src/model/Apps/scope-message.ts new file mode 100644 index 000000000000..3a16c5bf53cd --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/src/model/Apps/scope-message.ts @@ -0,0 +1,5 @@ +export default interface ScopeMessage { + icon: string; + type: string; + entities: string; +} diff --git a/src/Akeneo/Connectivity/Connection/front/src/model/Apps/wizard-data.ts b/src/Akeneo/Connectivity/Connection/front/src/model/Apps/wizard-data.ts new file mode 100644 index 000000000000..9e3520fac9f4 --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/src/model/Apps/wizard-data.ts @@ -0,0 +1,7 @@ +import ScopeMessage from './scope-message'; + +export interface AppWizardData { + appName: string; + appLogo: string; + scopeMessages: ScopeMessage[]; +} diff --git a/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizard/AppWizard.test.tsx b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizard/AppWizard.test.tsx index 50f03f3e2732..d53cb4d2bc89 100644 --- a/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizard/AppWizard.test.tsx +++ b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizard/AppWizard.test.tsx @@ -51,7 +51,7 @@ test('The wizard redirect to the marketplace when closed', async () => { await waitForElement(() => screen.getByAltText('MyApp')); act(() => { - userEvent.click(screen.getByTitle('akeneo_connectivity.connection.connect.apps.action.cancel')); + userEvent.click(screen.getByTitle('akeneo_connectivity.connection.connect.apps.wizard.action.cancel')); }); expect(historyMock.history.location.pathname).toBe('/connect/marketplace'); diff --git a/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizard/ScopeList.test.tsx b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizard/ScopeList.test.tsx deleted file mode 100644 index e602fda1edee..000000000000 --- a/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizard/ScopeList.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from 'react'; -import '@testing-library/jest-dom/extend-expect'; -import {screen} from '@testing-library/react'; -import fetchMock from 'jest-fetch-mock'; -import {renderWithProviders, historyMock} from '../../../../test-utils'; -import {ScopeList} from '@src/connect/components/AppWizard/ScopeList'; - -beforeEach(() => { - fetchMock.resetMocks(); - historyMock.reset(); -}); - -test('The scope list renders with scopes', () => { - const scopes = [ - { - icon: 'products', - type: 'read', - entities: 'products', - }, - ]; - - renderWithProviders(); - - expect( - screen.getByText('akeneo_connectivity.connection.connect.apps.authorize.title', {exact: false}) - ).toBeInTheDocument(); - expect( - screen.getByTitle('akeneo_connectivity.connection.connect.apps.authorize.scope.entities.products') - ).toBeInTheDocument(); - expect( - screen.getByText('akeneo_connectivity.connection.connect.apps.authorize.scope.entities.products') - ).toBeInTheDocument(); -}); - -test('The scope list still renders with unknown scopes', () => { - const scopes = [ - { - icon: 'foo', - type: 'read', - entities: 'foo', - }, - ]; - - renderWithProviders(); - - expect( - screen.getByText('akeneo_connectivity.connection.connect.apps.authorize.scope.entities.foo') - ).toBeInTheDocument(); -}); - -test('The scope list renders without scopes', () => { - renderWithProviders(); - - expect( - screen.getByText('akeneo_connectivity.connection.connect.apps.authorize.no_scope_title', {exact: false}) - ).toBeInTheDocument(); - expect(screen.getByTitle('akeneo_connectivity.connection.connect.apps.authorize.no_scope')).toBeInTheDocument(); -}); diff --git a/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizard/ScopeListContainer.test.tsx b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizard/ScopeListContainer.test.tsx new file mode 100644 index 000000000000..d9cbb9fd743e --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizard/ScopeListContainer.test.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import {screen} from '@testing-library/react'; +import fetchMock from 'jest-fetch-mock'; +import {renderWithProviders, historyMock} from '../../../../test-utils'; +import {ScopeListContainer} from '@src/connect/components/AppWizard/ScopeListContainer'; + +beforeEach(() => { + fetchMock.resetMocks(); + historyMock.reset(); +}); + +jest.mock('@src/connect/components/ScopeList', () => ({ + ScopeList: () =>
ScopeListComponent
, + ScopeItem: () =>
ScopeItemComponent
, +})); + +test('The scope list renders with scopes', () => { + const scopes = [ + { + icon: 'products', + type: 'read', + entities: 'products', + }, + ]; + + renderWithProviders(); + + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.authorize.title', {exact: false}) + ).toBeInTheDocument(); + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.authorize.helper') + ).toBeInTheDocument(); + expect(screen.queryByText('ScopeListComponent')).toBeInTheDocument(); +}); + +test('The scope list still renders with unknown scopes', () => { + const scopes = [ + { + icon: 'foo', + type: 'read', + entities: 'foo', + }, + ]; + + renderWithProviders(); + + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.authorize.helper') + ).toBeInTheDocument(); + expect(screen.queryByText('ScopeListComponent')).toBeInTheDocument(); +}); + +test('The scope list renders without scopes', () => { + renderWithProviders(); + + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.authorize.no_scope_title', { + exact: false, + }) + ).toBeInTheDocument(); + expect(screen.queryByText('ScopeItemComponent')).toBeInTheDocument(); + expect(screen.queryByText('ScopeListComponent')).not.toBeInTheDocument(); +}); diff --git a/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizardWithSteps/AppWizardWithSteps.test.tsx b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizardWithSteps/AppWizardWithSteps.test.tsx new file mode 100644 index 000000000000..8357dcae0ffc --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizardWithSteps/AppWizardWithSteps.test.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import {act, screen, waitForElement} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import {historyMock, mockFetchResponses, MockFetchResponses, renderWithProviders} from '../../../../test-utils'; +import {AppWizardWithSteps} from '@src/connect/components/AppWizardWithSteps/AppWizardWithSteps'; + +beforeEach(() => { + fetchMock.resetMocks(); + historyMock.reset(); +}); + +jest.mock('@src/connect/components/AppWizardWithSteps/Authorizations', () => ({ + Authorizations: () =>
authorizations-component
, +})); +jest.mock('@src/connect/components/AppWizardWithSteps/Permissions', () => ({ + Permissions: () =>
permissions-component
, +})); +test('The step wizard renders without error', async () => { + const fetchAppWizardDataResponses: MockFetchResponses = { + 'akeneo_connectivity_connection_apps_rest_get_wizard_data?clientId=8d8a7dc1-0827-4cc9-9ae5-577c6419230b': { + json: { + appName: 'MyApp', + appLogo: '', + scopeMessages: [], + }, + }, + }; + + mockFetchResponses({ + ...fetchAppWizardDataResponses, + }); + renderWithProviders(); + await waitForElement(() => screen.getByAltText('MyApp')); + expect(screen.queryByAltText('MyApp')).toBeInTheDocument(); + expect(screen.queryByText('authorizations-component')).toBeInTheDocument(); + expect(screen.queryByText('permissions-component')).not.toBeInTheDocument(); +}); + +test('The wizard redirect to the marketplace when closed', async () => { + const fetchAppWizardDataResponses: MockFetchResponses = { + 'akeneo_connectivity_connection_apps_rest_get_wizard_data?clientId=8d8a7dc1-0827-4cc9-9ae5-577c6419230b': { + json: { + appName: 'MyApp', + appLogo: '', + scopeMessages: [], + }, + }, + }; + + mockFetchResponses({ + ...fetchAppWizardDataResponses, + }); + + renderWithProviders(); + await waitForElement(() => screen.getByAltText('MyApp')); + + act(() => { + userEvent.click(screen.getByTitle('akeneo_connectivity.connection.connect.apps.wizard.action.cancel')); + }); + + expect(historyMock.history.location.pathname).toBe('/connect/marketplace'); +}); + +test('The wizard renders steps', async () => { + // TODO Add "Well done" step when done + const fetchAppWizardDataResponses: MockFetchResponses = { + 'akeneo_connectivity_connection_apps_rest_get_wizard_data?clientId=8d8a7dc1-0827-4cc9-9ae5-577c6419230b': { + json: { + appName: 'MyApp', + appLogo: '', + scopeMessages: [], + }, + }, + }; + + mockFetchResponses({ + ...fetchAppWizardDataResponses, + }); + renderWithProviders(); + await waitForElement(() => screen.getByAltText('MyApp')); + + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.action.allow_and_next') + ).toBeInTheDocument(); + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.action.next') + ).not.toBeInTheDocument(); + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.action.previous') + ).not.toBeInTheDocument(); + expect(screen.queryByText('authorizations-component')).toBeInTheDocument(); + expect(screen.queryByText('permissions-component')).not.toBeInTheDocument(); + + act(() => { + userEvent.click(screen.getByText('akeneo_connectivity.connection.connect.apps.wizard.action.allow_and_next')); + }); + + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.action.allow_and_next') + ).not.toBeInTheDocument(); + expect(screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.action.next')).toBeInTheDocument(); + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.action.previous') + ).toBeInTheDocument(); + expect(screen.queryByText('authorizations-component')).not.toBeInTheDocument(); + expect(screen.queryByText('permissions-component')).toBeInTheDocument(); + + act(() => { + userEvent.click(screen.getByText('akeneo_connectivity.connection.connect.apps.wizard.action.previous')); + }); + + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.action.allow_and_next') + ).toBeInTheDocument(); + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.action.next') + ).not.toBeInTheDocument(); + expect( + screen.queryByText('akeneo_connectivity.connection.connect.apps.wizard.action.previous') + ).not.toBeInTheDocument(); + expect(screen.queryByText('authorizations-component')).toBeInTheDocument(); + expect(screen.queryByText('permissions-component')).not.toBeInTheDocument(); +}); diff --git a/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizardWithSteps/Authorizations.test.tsx b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizardWithSteps/Authorizations.test.tsx new file mode 100644 index 000000000000..1dd7a4395ece --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizardWithSteps/Authorizations.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import {render, screen, waitForElement} from '@testing-library/react'; +import {ThemeProvider} from 'styled-components'; +import {pimTheme} from 'akeneo-design-system'; +import {Authorizations} from '@src/connect/components/AppWizardWithSteps/Authorizations'; + +jest.mock('@src/connect/components/AppWizard/ScopeListContainer', () => ({ + ScopeListContainer: () =>
ScopeListContainerComponent
, +})); +test('The authorizations step renders without error', async () => { + render( + + + + ); + await waitForElement(() => screen.getByText('akeneo_connectivity.connection.connect.apps.title')); + expect(screen.queryByText('akeneo_connectivity.connection.connect.apps.title')).toBeInTheDocument(); + expect(screen.queryByText('ScopeListContainerComponent')).toBeInTheDocument(); +}); diff --git a/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizardWithSteps/Permissions.test.tsx b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizardWithSteps/Permissions.test.tsx new file mode 100644 index 000000000000..5395933090d4 --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/AppWizardWithSteps/Permissions.test.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import {render, screen, waitForElement} from '@testing-library/react'; +import {Permissions} from '@src/connect/components/AppWizardWithSteps/Permissions'; + +test('The permissions step renders without error', async () => { + render(); + await waitForElement(() => screen.getByText('Hello permissions!')); +}); diff --git a/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/ScopeList.test.tsx b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/ScopeList.test.tsx new file mode 100644 index 000000000000..e42b2e72ba6f --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/components/ScopeList.test.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import fetchMock from 'jest-fetch-mock'; +import {historyMock, renderWithProviders} from '../../../test-utils'; +import {screen} from '@testing-library/react'; +import {ScopeList} from '@src/connect/components/ScopeList'; + +beforeEach(() => { + fetchMock.resetMocks(); + historyMock.reset(); +}); + +test('The scope list renders with scopes', () => { + const scopes = [ + { + icon: 'products', + type: 'read', + entities: 'products', + }, + ]; + + renderWithProviders(); + + expect( + screen.getByTitle('akeneo_connectivity.connection.connect.apps.scope.entities.products') + ).toBeInTheDocument(); + expect(screen.getByText('akeneo_connectivity.connection.connect.apps.scope.entities.products')).toBeInTheDocument(); +}); + +test('The scope list still renders with unknown scopes', () => { + const scopes = [ + { + icon: 'foo', + type: 'read', + entities: 'foo', + }, + ]; + + renderWithProviders(); + + expect(screen.getByText('akeneo_connectivity.connection.connect.apps.scope.entities.foo')).toBeInTheDocument(); +}); + +test('The scope list renders without scopes', () => { + renderWithProviders(); + + expect(screen.getByTestId('scope-list')).toBeInTheDocument(); + expect(screen.getByTestId('scope-list')).toBeEmptyDOMElement(); +}); diff --git a/src/Akeneo/Connectivity/Connection/front/tests/src/connect/pages/AppAuthorizePage.test.tsx b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/pages/AppAuthorizePage.test.tsx new file mode 100644 index 000000000000..75db9f77b543 --- /dev/null +++ b/src/Akeneo/Connectivity/Connection/front/tests/src/connect/pages/AppAuthorizePage.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import fetchMock from 'jest-fetch-mock'; +import {historyMock, renderWithProviders} from '../../../test-utils'; +import {screen, waitForElement} from '@testing-library/react'; +import {AppAuthorizePage} from '@src/connect/pages/AppAuthorizePage'; +import {FeatureFlagsContext} from '@src/shared/feature-flags'; +import {useLocation} from 'react-router-dom'; + +beforeEach(() => { + fetchMock.resetMocks(); + historyMock.reset(); +}); + +jest.mock('@src/connect/components/AppWizard/AppWizard', () => ({ + AppWizard: () =>
AppWizard
, +})); +jest.mock('@src/connect/components/AppWizardWithSteps/AppWizardWithSteps', () => ({ + AppWizardWithSteps: () =>
AppWizardWithSteps
, +})); +jest.mock('@src/connect/components/AuthorizeClientError', () => ({ + AuthorizeClientError: () =>
AuthorizeClientError
, +})); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn().mockImplementation(() => ({ + search: '?client_id=8d8a7dc1-0827-4cc9-9ae5-577c6419230b', + })), +})); + +test('The wizard renders app wizard', async () => { + renderWithProviders(); + await waitForElement(() => screen.getByText('AppWizard')); + + expect(screen.queryByText('AppWizard')).toBeInTheDocument(); + expect(screen.queryByText('AppWizardWithSteps')).not.toBeInTheDocument(); + expect(screen.queryByText('AuthorizeClientError')).not.toBeInTheDocument(); +}); + +test('The wizard renders app wizard with steps', async () => { + renderWithProviders( + true}}> + + + ); + await waitForElement(() => screen.getByText('AppWizardWithSteps')); + + expect(screen.queryByText('AppWizard')).not.toBeInTheDocument(); + expect(screen.queryByText('AuthorizeClientError')).not.toBeInTheDocument(); + expect(screen.queryByText('AppWizardWithSteps')).toBeInTheDocument(); +}); + +test('The wizard renders client error', async () => { + (useLocation as jest.Mock).mockImplementationOnce(() => ({ + search: '?error=toto', + })); + renderWithProviders(); + await waitForElement(() => screen.getByText('AuthorizeClientError')); + + expect(screen.queryByText('AuthorizeClientError')).toBeInTheDocument(); + expect(screen.queryByText('AppWizard')).not.toBeInTheDocument(); + expect(screen.queryByText('AppWizardWithSteps')).not.toBeInTheDocument(); +}); diff --git a/src/Akeneo/Connectivity/Connection/front/tests/test-utils.tsx b/src/Akeneo/Connectivity/Connection/front/tests/test-utils.tsx index 6690f169612a..28e9b122ecfb 100644 --- a/src/Akeneo/Connectivity/Connection/front/tests/test-utils.tsx +++ b/src/Akeneo/Connectivity/Connection/front/tests/test-utils.tsx @@ -3,7 +3,7 @@ import {render} from '@testing-library/react'; import React, {FC} from 'react'; import {create} from 'react-test-renderer'; import {ThemeProvider} from 'styled-components'; -import {theme} from '../src/common/styled-with-theme'; +import {theme} from '@src/common/styled-with-theme'; import fetchMock from 'jest-fetch-mock'; import {Router} from 'react-router-dom'; import {createMemoryHistory} from 'history';