From d575e8402d480b6efa513db4cb856aee9e0a1fa2 Mon Sep 17 00:00:00 2001 From: Klaudija Ljevar Date: Fri, 23 Feb 2024 09:29:56 +0100 Subject: [PATCH] AppBar changes (phase 2) (#3403) * chore: creating project-stamp component * chore: removing old ui and improving project-stamp * chore: styling project switcher * chore: update all ui-kit dependencies * chore: setting up the width and height of selectors * chore: adding style for smaller DotIcon * chore: styling the stamps inside the project selector * chore: adjusting the fonts for project selector * chore: adjusting the style of stamps * chore: adjusting the layout of the appbar components * chore: adding the labels to project and locale switchers * chore: replace old designtokens * chore: small UI changes * chore: replace the GroupHeading props * chore: changeset * chore: adding stamp dependency * chore: added the dialog for locale explanation * chore: changeset * fix: the react-select import * fix: typescript error in CustomGroupHeading component * chore: change the react-select import * chore: removing the unneccessary files * chore: replacing one Stamp component with individual ones for each state * refactor(application-components): refactor app bar selectors styling * fix(application-shell): fix typing issues * chore: improving project stamps * chore: removing the 'export' typo * chore: test update * chore: code improvements * chore: small code improvement * chore: adding the VTRs and updating the ProjectStamp label * chore: adding the translation to locale switcher label * chore: updating the README.md * chore: adding the translation to project switcher label * chore: adding the project-stamp test * chore: adding the locale switcher test * chore: updated the copy of the locales dialog * chore: adding the test for stamps in project-switcher * chore: copy improvements * chore: added the needed dependencies in packgage.json files * chore: adding the types * chore: stamp improvements * chore: include `useModalState` hook * chore: updating the translations * chore: removing the translations * chore: updated changeset * chore: updating messages.ts * chore: add a prop type * chore: code improvements * chore: updating the project-switcher test * chore: prop type * chore: updating the prop type * chore: revert the prop types changes * chore: updating the title of InfoDialog * chore: updating dialogLocaleDescription message * refactor(application-shell): extract helper function out of the test implementation * chore: updated translation * chore: updated test * chore: improved locale description copy --------- Co-authored-by: Carlos Cortizas --- .changeset/kind-years-laugh.md | 11 ++ packages/application-components/package.json | 1 + .../src/components/project-stamp/messages.ts | 21 +++ .../project-stamp/project-stamp.spec.tsx | 30 ++++ .../project-stamp/project-stamp.tsx | 71 ++++++++ packages/application-components/src/index.ts | 3 + .../src/types/generated/mc.ts | 14 +- packages/application-shell/package.json | 1 + .../src/components/app-bar/app-bar.tsx | 50 ++++-- .../application-shell.spec.js | 2 +- .../fetch-user/fetch-user.mc.graphql | 1 + .../locale-switcher/locale-switcher.spec.tsx | 50 ++++++ .../locale-switcher/locale-switcher.tsx | 94 +++++++--- .../components/locale-switcher/messages.ts | 13 +- .../components/project-switcher/messages.ts | 2 +- .../project-switcher-test-utils.ts | 4 +- .../project-switcher.spec.tsx | 55 +++++- .../project-switcher/project-switcher.tsx | 160 ++++++++---------- .../src/types/generated/mc.ts | 14 +- packages/i18n/README.md | 2 +- packages/i18n/data/core.json | 10 +- packages/i18n/data/en.json | 4 +- .../branch-on-permissions.spec.tsx | 1 + .../permissions/src/types/generated/mc.ts | 14 +- playground/src/i18n/data/core.json | 17 +- playground/src/i18n/data/de.json | 2 - playground/src/i18n/data/en.json | 4 +- playground/src/i18n/data/es.json | 4 +- pnpm-lock.yaml | 52 ++++-- schemas/mc.json | 72 ++++++++ test-data/types/generated/mc.ts | 14 +- .../project-stamp.visualroute.tsx | 21 +++ .../project-stamp/project-stamp.visualspec.ts | 13 ++ 33 files changed, 654 insertions(+), 173 deletions(-) create mode 100644 .changeset/kind-years-laugh.md create mode 100644 packages/application-components/src/components/project-stamp/messages.ts create mode 100644 packages/application-components/src/components/project-stamp/project-stamp.spec.tsx create mode 100644 packages/application-components/src/components/project-stamp/project-stamp.tsx create mode 100644 packages/application-shell/src/components/locale-switcher/locale-switcher.spec.tsx create mode 100644 visual-testing-app/src/components/project-stamp/project-stamp.visualroute.tsx create mode 100644 visual-testing-app/src/components/project-stamp/project-stamp.visualspec.ts diff --git a/.changeset/kind-years-laugh.md b/.changeset/kind-years-laugh.md new file mode 100644 index 0000000000..8f2edda465 --- /dev/null +++ b/.changeset/kind-years-laugh.md @@ -0,0 +1,11 @@ +--- +'@commercetools-frontend/application-shell-connectors': patch +'@commercetools-frontend/application-components': patch +'@commercetools-frontend/application-shell': patch +'@commercetools-frontend/permissions': patch +'@commercetools-local/visual-testing-app': patch +'@commercetools-frontend/i18n': patch +'@commercetools-local/playground': patch +--- + +New UI of the App Bar selectors. diff --git a/packages/application-components/package.json b/packages/application-components/package.json index d21e4a56f7..ce71ad1221 100644 --- a/packages/application-components/package.json +++ b/packages/application-components/package.json @@ -39,6 +39,7 @@ "@commercetools-frontend/sentry": "22.17.2", "@commercetools-uikit/accessible-button": "^18.1.0", "@commercetools-uikit/card": "^18.1.0", + "@commercetools-uikit/stamp": "^18.1.0", "@commercetools-uikit/constraints": "^18.1.0", "@commercetools-uikit/design-system": "^18.1.0", "@commercetools-uikit/flat-button": "^18.1.0", diff --git a/packages/application-components/src/components/project-stamp/messages.ts b/packages/application-components/src/components/project-stamp/messages.ts new file mode 100644 index 0000000000..6962ca705c --- /dev/null +++ b/packages/application-components/src/components/project-stamp/messages.ts @@ -0,0 +1,21 @@ +import { defineMessages } from 'react-intl'; + +export default defineMessages({ + ProjectProduction: { + id: 'ProjectStamp.production', + defaultMessage: 'Production', + }, + ProjectSuspended: { + id: 'ProjectStamp.suspended', + defaultMessage: 'Suspended', + }, + ProjectExpired: { + id: 'ProjectStamp.expired', + defaultMessage: 'Trial expired', + }, + ProjectWillExpire: { + id: 'ProjectStamp.willExpire', + defaultMessage: + '{daysLeft, select, 0 {Trial ends today} 1 {Trial ends in 1 day} other {Trial ends in {daysLeft} days}}', + }, +}); diff --git a/packages/application-components/src/components/project-stamp/project-stamp.spec.tsx b/packages/application-components/src/components/project-stamp/project-stamp.spec.tsx new file mode 100644 index 0000000000..b885e2ce68 --- /dev/null +++ b/packages/application-components/src/components/project-stamp/project-stamp.spec.tsx @@ -0,0 +1,30 @@ +import { screen, renderComponent } from '../../test-utils'; +import ProjectStamp from './project-stamp'; + +describe('rendering', () => { + it('should render ProjectStamp - IsProduction', () => { + renderComponent(); + + expect(screen.getByText('Production')).toBeInTheDocument(); + }); + it('should render ProjectStamp - IsExpired', () => { + renderComponent(); + + expect(screen.getByText('Trial expired')).toBeInTheDocument(); + }); + it('should render ProjectStamp - WillExpire with 0 days left', () => { + renderComponent(); + + expect(screen.getByText('Trial ends today')).toBeInTheDocument(); + }); + it('should render ProjectStamp - WillExpire with 1 days left', () => { + renderComponent(); + + expect(screen.getByText('Trial ends in 1 day')).toBeInTheDocument(); + }); + it('should render ProjectStamp - WillExpire with 4 days left', () => { + renderComponent(); + + expect(screen.getByText('Trial ends in 4 days')).toBeInTheDocument(); + }); +}); diff --git a/packages/application-components/src/components/project-stamp/project-stamp.tsx b/packages/application-components/src/components/project-stamp/project-stamp.tsx new file mode 100644 index 0000000000..1f5607fa0f --- /dev/null +++ b/packages/application-components/src/components/project-stamp/project-stamp.tsx @@ -0,0 +1,71 @@ +import { ReactElement } from 'react'; +import { css } from '@emotion/react'; +import { MessageDescriptor, useIntl } from 'react-intl'; +import { DotIcon } from '@commercetools-uikit/icons'; +import Stamp, { TTone } from '@commercetools-uikit/stamp'; +import messages from './messages'; + +type TCustomStampProps = { + tone: TTone; + label: MessageDescriptor & { values?: Record }; + icon?: ReactElement; +}; +function CustomStamp(props: TCustomStampProps) { + const intl = useIntl(); + const { values, ...message } = props.label; + return ( + + ); +} + +const IsProduction = () => ( + + + + } + /> +); + +const IsSuspended = () => ( + +); + +const IsExpired = () => ( + +); + +const WillExpire = (props: { daysLeft: number }) => ( + +); + +const ProjectStamp = { + IsProduction, + IsSuspended, + IsExpired, + WillExpire, +}; + +export default ProjectStamp; diff --git a/packages/application-components/src/index.ts b/packages/application-components/src/index.ts index b03aac2a5e..a02a9b0f94 100644 --- a/packages/application-components/src/index.ts +++ b/packages/application-components/src/index.ts @@ -48,6 +48,9 @@ export { default as Drawer } from './components/drawer'; export { default as CustomViewLoader } from './components/custom-views/custom-view-loader'; export { default as CustomViewsSelector } from './components/custom-views/custom-views-selector'; +// Stamps for the project states +export { default as ProjectStamp } from './components/project-stamp/project-stamp'; + // Utilities export { default as PortalsContainer } from './components/portals-container'; export { default as useModalState } from './hooks/use-modal-state'; diff --git a/packages/application-shell-connectors/src/types/generated/mc.ts b/packages/application-shell-connectors/src/types/generated/mc.ts index 1dd175d231..3c6a76a513 100644 --- a/packages/application-shell-connectors/src/types/generated/mc.ts +++ b/packages/application-shell-connectors/src/types/generated/mc.ts @@ -425,6 +425,7 @@ export type TQuery = { release?: Maybe; releases?: Maybe; storeOAuthScopes: Array; + systemStatus: TSystemStatus; }; @@ -591,6 +592,17 @@ export type TSupportedStoreScope = { name: Scalars['String']; }; +export enum TSystemOperabilityStatus { + Degraded = 'DEGRADED', + Operational = 'OPERATIONAL', + Outage = 'OUTAGE' +} + +export type TSystemStatus = { + __typename?: 'SystemStatus'; + status: TSystemOperabilityStatus; +}; + export type TUser = TMetaData & { __typename?: 'User'; businessRole?: Maybe; @@ -659,7 +671,7 @@ export type TFetchProjectQuery = { __typename?: 'Query', project?: { __typename? export type TFetchLoggedInUserQueryVariables = Exact<{ [key: string]: never; }>; -export type TFetchLoggedInUserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email: string, createdAt: string, gravatarHash: string, firstName: string, lastName: string, language: string, numberFormat: string, timeZone?: string | null, launchdarklyTrackingId: string, launchdarklyTrackingGroup: string, launchdarklyTrackingSubgroup?: string | null, launchdarklyTrackingTeam?: Array | null, launchdarklyTrackingTenant: string, launchdarklyTrackingCloudEnvironment: string, defaultProjectKey?: string | null, businessRole?: string | null, projects: { __typename?: 'ProjectQueryResult', total: number, results: Array<{ __typename?: 'Project', name: string, key: string, suspension: { __typename?: 'ProjectSuspension', isActive: boolean }, expiry: { __typename?: 'ProjectExpiry', isActive: boolean } }> }, idTokenUserInfo?: { __typename?: 'IdTokenUserInfo', iss: string, sub: string, aud: string, exp: number, iat?: number | null, email?: string | null, name?: string | null, additionalClaims?: string | null } | null } | null }; +export type TFetchLoggedInUserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email: string, createdAt: string, gravatarHash: string, firstName: string, lastName: string, language: string, numberFormat: string, timeZone?: string | null, launchdarklyTrackingId: string, launchdarklyTrackingGroup: string, launchdarklyTrackingSubgroup?: string | null, launchdarklyTrackingTeam?: Array | null, launchdarklyTrackingTenant: string, launchdarklyTrackingCloudEnvironment: string, defaultProjectKey?: string | null, businessRole?: string | null, projects: { __typename?: 'ProjectQueryResult', total: number, results: Array<{ __typename?: 'Project', name: string, key: string, isProductionProject: boolean, suspension: { __typename?: 'ProjectSuspension', isActive: boolean }, expiry: { __typename?: 'ProjectExpiry', isActive: boolean } }> }, idTokenUserInfo?: { __typename?: 'IdTokenUserInfo', iss: string, sub: string, aud: string, exp: number, iat?: number | null, email?: string | null, name?: string | null, additionalClaims?: string | null } | null } | null }; export type TFetchUserProjectsQueryVariables = Exact<{ [key: string]: never; }>; diff --git a/packages/application-shell/package.json b/packages/application-shell/package.json index c7c9efec2f..12e574993b 100644 --- a/packages/application-shell/package.json +++ b/packages/application-shell/package.json @@ -52,6 +52,7 @@ "@commercetools-uikit/design-system": "^18.1.0", "@commercetools-uikit/flat-button": "^18.1.0", "@commercetools-uikit/icons": "^18.1.0", + "@commercetools-uikit/icon-button": "^18.1.0", "@commercetools-uikit/loading-spinner": "^18.1.0", "@commercetools-uikit/notifications": "^18.1.0", "@commercetools-uikit/primary-button": "^18.1.0", diff --git a/packages/application-shell/src/components/app-bar/app-bar.tsx b/packages/application-shell/src/components/app-bar/app-bar.tsx index f7b033dac8..6c61cd07c9 100644 --- a/packages/application-shell/src/components/app-bar/app-bar.tsx +++ b/packages/application-shell/src/components/app-bar/app-bar.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/react'; +import { ProjectStamp } from '@commercetools-frontend/application-components'; import { designTokens as uikitDesignTokens } from '@commercetools-uikit/design-system'; import Spacings from '@commercetools-uikit/spacings'; import { CONTAINERS, DIMENSIONS } from '../../constants'; @@ -47,30 +48,57 @@ const AppBar = (props: Props) => { `} > - +
{(() => { if (!props.user) { return ; } // The `` should be rendered only if the // user is fetched and the user has projects while the app runs in an project context. - if (props.user.projects.total > 0 && props.projectKeyFromUrl) + if (props.user.projects.total > 0 && props.projectKeyFromUrl) { + const selectedProject = props.user.projects.results.find( + (project) => project.key === props.projectKeyFromUrl + ); return ( - +
+ {selectedProject?.isProductionProject && ( +
+ +
+ )} + +
); + } if (!props.user.defaultProjectKey) return null; return ; })()} {/* This node is used by a react portal */}
- +
{ describe('when switching project', () => { it('should render app for new project', async () => { renderApp(); - const input = await screen.findByLabelText('Projects menu'); + const input = await screen.findByLabelText('Projects'); fireEvent.focus(input); fireEvent.keyDown(input, { key: 'ArrowDown' }); diff --git a/packages/application-shell/src/components/fetch-user/fetch-user.mc.graphql b/packages/application-shell/src/components/fetch-user/fetch-user.mc.graphql index f120de1260..b41a50ee0c 100644 --- a/packages/application-shell/src/components/fetch-user/fetch-user.mc.graphql +++ b/packages/application-shell/src/components/fetch-user/fetch-user.mc.graphql @@ -28,6 +28,7 @@ query FetchLoggedInUser { expiry { isActive } + isProductionProject } } idTokenUserInfo { diff --git a/packages/application-shell/src/components/locale-switcher/locale-switcher.spec.tsx b/packages/application-shell/src/components/locale-switcher/locale-switcher.spec.tsx new file mode 100644 index 0000000000..74dd874542 --- /dev/null +++ b/packages/application-shell/src/components/locale-switcher/locale-switcher.spec.tsx @@ -0,0 +1,50 @@ +import { render, fireEvent, waitFor, screen } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import LocaleSwitcher from './locale-switcher'; + +const setProjectDataLocale = jest.fn(); +const availableLocales = ['en', 'de', 'fr']; + +const renderLocaleSwitcher = () => { + return render( + + + + ); +}; + +describe('LocaleSwitcher', () => { + it('should render and handle locale selection', async () => { + renderLocaleSwitcher(); + const input = await screen.findByLabelText('Locales'); + fireEvent.focus(input); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('fr')); + + await waitFor(() => { + expect(setProjectDataLocale).toHaveBeenCalledWith('fr'); + }); + }); + it('should open and close the locale dialog', async () => { + renderLocaleSwitcher(); + const input = await screen.findByLabelText('Locales'); + fireEvent.focus(input); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + const iconButton = await screen.findByRole('button', { + name: 'Locales info', + }); + fireEvent.click(iconButton); + + // expect to see the dialog opens after clicking the icon button + const dialogText = await screen.findByText('Selecting a data locale'); + expect(dialogText).toBeInTheDocument(); + + // close the dialog + fireEvent.click(screen.getByRole('button', { name: 'Close dialog' })); + expect(dialogText).not.toBeInTheDocument(); + }); +}); diff --git a/packages/application-shell/src/components/locale-switcher/locale-switcher.tsx b/packages/application-shell/src/components/locale-switcher/locale-switcher.tsx index 280fde50ef..f5c269d3d2 100644 --- a/packages/application-shell/src/components/locale-switcher/locale-switcher.tsx +++ b/packages/application-shell/src/components/locale-switcher/locale-switcher.tsx @@ -1,16 +1,23 @@ import { useCallback } from 'react'; -import { css } from '@emotion/react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import type { SingleValueProps, ValueContainerProps, MenuListProps, + GroupHeadingProps, } from 'react-select'; import { components } from 'react-select'; +import { + InfoDialog, + useModalState, +} from '@commercetools-frontend/application-components'; import AccessibleHidden from '@commercetools-uikit/accessible-hidden'; import { designTokens } from '@commercetools-uikit/design-system'; -import { WorldIcon } from '@commercetools-uikit/icons'; +import IconButton from '@commercetools-uikit/icon-button'; +import { WorldIcon, InformationIcon } from '@commercetools-uikit/icons'; import SelectInput from '@commercetools-uikit/select-input'; +import Spacings from '@commercetools-uikit/spacings'; +import Text from '@commercetools-uikit/text'; import messages from './messages'; type Props = { @@ -23,24 +30,10 @@ const LOCALE_SWITCHER_LABEL_ID = 'locale-switcher-label'; export const SingleValue = (props: SingleValueProps) => { return ( -
+ - - {props.children} - -
+ {props.children} + ); }; SingleValue.displayName = 'SingleValue'; @@ -59,14 +52,51 @@ const CustomMenuList = (props: MenuListProps) => { return {props.children}; }; +const CustomGroupHeading = ( + props: GroupHeadingProps & { setIsOpen: (value: boolean) => void } +) => { + const { setIsOpen, ...groupProps } = props; + return ( + <> + + + {groupProps.children} + } + label="Locales info" + size="small" + onClick={() => setIsOpen(true)} + /> + + + + ); +}; +CustomGroupHeading.displayName = 'CustomGroupHeading'; + const LocaleSwitcher = (props: Props) => { + const { isModalOpen, openModal, closeModal } = useModalState(); const { setProjectDataLocale } = props; + const getNewLine = () =>
; + const intl = useIntl(); + const handleSelection = useCallback( (event) => { setProjectDataLocale(event.target.value); }, [setProjectDataLocale] ); + + const localeOptions = [ + { + label: , + options: props.availableLocales.map((locale) => ({ + label: locale, + value: locale, + })), + }, + ]; + return (
@@ -79,14 +109,14 @@ const LocaleSwitcher = (props: Props) => { name="locale-switcher" aria-labelledby={LOCALE_SWITCHER_LABEL_ID} onChange={handleSelection} - options={props.availableLocales.map((locale) => ({ - label: locale, - value: locale, - }))} + options={localeOptions} components={{ SingleValue, ValueContainer: PatchedValueContainer, MenuList: CustomMenuList, + GroupHeading: (groupProps) => ( + + ), }} isClearable={false} backspaceRemovesValue={false} @@ -94,7 +124,23 @@ const LocaleSwitcher = (props: Props) => { horizontalConstraint={'auto'} appearance="quiet" maxMenuHeight={360} + minMenuWidth={3} /> + {/* Dialog that explains the locales */} + + +
); }; diff --git a/packages/application-shell/src/components/locale-switcher/messages.ts b/packages/application-shell/src/components/locale-switcher/messages.ts index a494b8da50..45a0fe6d54 100644 --- a/packages/application-shell/src/components/locale-switcher/messages.ts +++ b/packages/application-shell/src/components/locale-switcher/messages.ts @@ -4,6 +4,17 @@ export default defineMessages({ localesLabel: { id: 'LocaleSwitcher.localesLabel', description: 'The label for project dropdown switcher', - defaultMessage: 'Locales menu', + defaultMessage: 'Locales', + }, + dialogLocaleTitle: { + id: 'LocaleSwitcher.dialogLocaleTitle', + description: 'The title for the data locale dialog', + defaultMessage: 'Selecting a data locale', + }, + dialogLocaleDescription: { + id: 'LocaleSwitcher.dialogLocaleDescription', + description: 'The description for the data locale dialog', + defaultMessage: + "The selected data locale will serve as the default setting for all localized fields within the Merchant Center, including names, descriptions, and other localized attributes. It's important to note that this selection does not affect the interface language of the Merchant Center or any data formatting options. To modify these settings, navigate to your user profile.", }, }); diff --git a/packages/application-shell/src/components/project-switcher/messages.ts b/packages/application-shell/src/components/project-switcher/messages.ts index 4a884e3219..7a2d696b85 100644 --- a/packages/application-shell/src/components/project-switcher/messages.ts +++ b/packages/application-shell/src/components/project-switcher/messages.ts @@ -4,7 +4,7 @@ export default defineMessages({ projectsLabel: { id: 'ProjectSwitcher.projectsLabel', description: 'The label for project dropdown switcher', - defaultMessage: 'Projects menu', + defaultMessage: 'Projects', }, searchPlaceholder: { id: 'ProjectSwitcher.searchPlaceholder', diff --git a/packages/application-shell/src/components/project-switcher/project-switcher-test-utils.ts b/packages/application-shell/src/components/project-switcher/project-switcher-test-utils.ts index 102f0f41b2..9cc1ff36db 100644 --- a/packages/application-shell/src/components/project-switcher/project-switcher-test-utils.ts +++ b/packages/application-shell/src/components/project-switcher/project-switcher-test-utils.ts @@ -5,6 +5,7 @@ type CreateGraphqlResponseForProjectsQueryOptions = { numberOfProjects?: number; getIsSuspended?: (key: string) => boolean; getIsExpired?: (key: string) => boolean; + getIsProduction?: (key: string) => boolean; }; const falsy = () => false; @@ -13,6 +14,7 @@ export const createGraphqlResponseForProjectsQuery = ({ numberOfProjects = 4, getIsSuspended = falsy, getIsExpired = falsy, + getIsProduction = falsy, }: CreateGraphqlResponseForProjectsQueryOptions = {}) => ({ request: { query: ProjectsQuery, @@ -42,7 +44,7 @@ export const createGraphqlResponseForProjectsQuery = ({ __typename: 'ProjectExpiry', isActive: getIsExpired(key), }, - isProductionProject: false, + isProductionProject: getIsProduction(key), }; }), }, diff --git a/packages/application-shell/src/components/project-switcher/project-switcher.spec.tsx b/packages/application-shell/src/components/project-switcher/project-switcher.spec.tsx index 21ac844ae8..7a41ece234 100644 --- a/packages/application-shell/src/components/project-switcher/project-switcher.spec.tsx +++ b/packages/application-shell/src/components/project-switcher/project-switcher.spec.tsx @@ -1,5 +1,11 @@ import { mocked } from 'jest-mock'; -import { screen, renderApp, fireEvent, waitFor } from '../../test-utils'; +import { + screen, + renderApp, + fireEvent, + waitFor, + within, +} from '../../test-utils'; import { location } from '../../utils/location'; import ProjectSwitcher from './project-switcher'; import { createGraphqlResponseForProjectsQuery } from './project-switcher-test-utils'; @@ -9,7 +15,8 @@ jest.mock('../../utils/location'); const render = () => { const mockedRequest = [ createGraphqlResponseForProjectsQuery({ - getIsSuspended: (key) => key === 'key-2', + getIsProduction: (key) => key === 'key-1' || key === 'key-3', + getIsSuspended: (key) => key === 'key-2' || key === 'key-3', getIsExpired: (key) => key === 'key-3', }), ]; @@ -19,6 +26,19 @@ const render = () => { }); }; +function verifyProjectOptionStamps( + projectOption: HTMLElement, + expectedText: string[] | null[] +) { + const stamps = within(projectOption).queryAllByText( + /Production|Suspended|Trial expired/i + ); + + stamps.forEach((stamp, index) => { + expect(stamp.textContent).toEqual(expectedText[index]); + }); +} + describe('rendering', () => { beforeEach(() => { mocked(location.replace).mockClear(); @@ -26,7 +46,7 @@ describe('rendering', () => { it('should search and select a project', async () => { render(); - const input = await screen.findByLabelText('Projects menu'); + const input = await screen.findByLabelText('Projects'); fireEvent.focus(input); fireEvent.change(input, { target: { value: 'key-1' } }); fireEvent.keyDown(input, { key: 'Enter', keyCode: 13, which: 13 }); @@ -37,7 +57,7 @@ describe('rendering', () => { }); it('should see no results message when search does not match any project', async () => { render(); - const input = await screen.findByLabelText('Projects menu'); + const input = await screen.findByLabelText('Projects'); fireEvent.focus(input); fireEvent.change(input, { target: { value: 'not existing' } }); @@ -47,10 +67,10 @@ describe('rendering', () => { }); it('should prevent clicking on a suspended project', async () => { render(); - const input = await screen.findByLabelText('Projects menu'); + const input = await screen.findByLabelText('Projects'); fireEvent.focus(input); fireEvent.keyDown(input, { key: 'ArrowDown' }); - fireEvent.click(screen.getByText(/Suspended/i)); + fireEvent.click((await screen.findAllByText(/Suspended/i))[0]); await waitFor(() => expect(mocked(location.replace)).not.toHaveBeenCalled() @@ -58,7 +78,7 @@ describe('rendering', () => { }); it('should prevent clicking on an expired project', async () => { render(); - const input = await screen.findByLabelText('Projects menu'); + const input = await screen.findByLabelText('Projects'); fireEvent.focus(input); fireEvent.keyDown(input, { key: 'ArrowDown' }); fireEvent.click(screen.getByText(/Expired/i)); @@ -67,4 +87,25 @@ describe('rendering', () => { expect(mocked(location.replace)).not.toHaveBeenCalled() ); }); + it('should render the expected stamps for each project', async () => { + render(); + const input = await screen.findByLabelText('Projects'); + fireEvent.focus(input); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + + const swticherProjectsOptions = await screen.findAllByRole('option'); + + // We define which stamps are expected for each switcher option + // The stamps are ordered so we can also verified they're rendered in the correct order + const expectedOrderedStamps = [ + [null], // First option has no stamps + ['Production'], // Second option has only one "Production" stamp + ['Suspended'], // Third option has only one "Suspended" stamp + ['Production', 'Suspended', 'Trial expired'], // Fourth option has all three stamps + ]; + + swticherProjectsOptions.forEach((projectOption, index) => { + verifyProjectOptionStamps(projectOption, expectedOrderedStamps[index]); + }); + }); }); diff --git a/packages/application-shell/src/components/project-switcher/project-switcher.tsx b/packages/application-shell/src/components/project-switcher/project-switcher.tsx index 046b228ff0..592022f69a 100644 --- a/packages/application-shell/src/components/project-switcher/project-switcher.tsx +++ b/packages/application-shell/src/components/project-switcher/project-switcher.tsx @@ -8,6 +8,7 @@ import type { ControlProps, } from 'react-select'; import { components } from 'react-select'; +import { ProjectStamp } from '@commercetools-frontend/application-components'; import { useMcQuery, oidcStorage, @@ -17,8 +18,9 @@ import { GRAPHQL_TARGETS } from '@commercetools-frontend/constants'; import { reportErrorToSentry } from '@commercetools-frontend/sentry'; import AccessibleHidden from '@commercetools-uikit/accessible-hidden'; import { designTokens } from '@commercetools-uikit/design-system'; -import { ErrorIcon } from '@commercetools-uikit/icons'; import SelectInput from '@commercetools-uikit/select-input'; +import Spacings from '@commercetools-uikit/spacings'; +import Text from '@commercetools-uikit/text'; import type { TProject, TFetchUserProjectsQuery, @@ -42,110 +44,89 @@ type OptionType = Pick< const PROJECT_SWITCHER_LABEL_ID = 'project-switcher-label'; -export const ValueContainer = ({ ...restProps }: ValueContainerProps) => { +export const ValueContainer = ({ + children, + ...restProps +}: ValueContainerProps) => { return ( -
-
- - {restProps.children} - -
-
+ + + {children} + + ); }; +type TProjectStampsListProps = Pick< + TProject, + 'isProductionProject' | 'suspension' | 'expiry' +>; +const ProjectStampsList = (props: TProjectStampsListProps) => ( + + {props.isProductionProject && } + {props.suspension && props.suspension.isActive && ( + + )} + {props.expiry && props.expiry.isActive && } + {props.expiry && Boolean(props.expiry.daysLeft) && ( + + )} + +); + export const ProjectSwitcherOption = (props: OptionProps) => { const project = props.data as OptionType; + return ( -
+
- {project.name} - {props.isDisabled && ( - - - - )} -
-
- {project.key} -
- {project.suspension && project.suspension.isActive && ( -
- -
- )} - {project.expiry && project.expiry.isActive && ( -
- -
- )} -
+ {project.name} + + + {project.key} + +
+ + +
); }; -const mapProjectsToOptions = memoize((projects) => - projects.map((project: TProject) => ({ - key: project.key, - name: project.name, - label: project.name, - value: project.key, - suspension: project.suspension, - expiry: project.expiry, - isProductionProject: project.isProductionProject, - })) -); +const mapProjectsToOptions = memoize((projects) => { + return [ + { + label: , + options: projects.map((project: TProject) => ({ + key: project.key, + name: project.name, + label: project.name, + value: project.key, + suspension: project.suspension, + expiry: project.expiry, + isProductionProject: project.isProductionProject, + })), + }, + ]; +}); const CustomMenuList = (props: MenuListProps) => { return ( -
+
{props.children}
); @@ -203,7 +184,10 @@ const ProjectSwitcher = (props: Props) => { } }} options={ - data && data.user && mapProjectsToOptions(data.user.projects.results) + (data && + data.user && + mapProjectsToOptions(data.user.projects.results)) || + [] } isOptionDisabled={(option) => { const project = option as OptionType; @@ -221,7 +205,9 @@ const ProjectSwitcher = (props: Props) => { noOptionsMessage={() => intl.formatMessage(messages.noResults)} horizontalConstraint={'auto'} appearance="quiet" - maxMenuHeight={360} + maxMenuHeight={380} + maxMenuWidth={8} + minMenuWidth={8} />
); diff --git a/packages/application-shell/src/types/generated/mc.ts b/packages/application-shell/src/types/generated/mc.ts index 1dd175d231..3c6a76a513 100644 --- a/packages/application-shell/src/types/generated/mc.ts +++ b/packages/application-shell/src/types/generated/mc.ts @@ -425,6 +425,7 @@ export type TQuery = { release?: Maybe; releases?: Maybe; storeOAuthScopes: Array; + systemStatus: TSystemStatus; }; @@ -591,6 +592,17 @@ export type TSupportedStoreScope = { name: Scalars['String']; }; +export enum TSystemOperabilityStatus { + Degraded = 'DEGRADED', + Operational = 'OPERATIONAL', + Outage = 'OUTAGE' +} + +export type TSystemStatus = { + __typename?: 'SystemStatus'; + status: TSystemOperabilityStatus; +}; + export type TUser = TMetaData & { __typename?: 'User'; businessRole?: Maybe; @@ -659,7 +671,7 @@ export type TFetchProjectQuery = { __typename?: 'Query', project?: { __typename? export type TFetchLoggedInUserQueryVariables = Exact<{ [key: string]: never; }>; -export type TFetchLoggedInUserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email: string, createdAt: string, gravatarHash: string, firstName: string, lastName: string, language: string, numberFormat: string, timeZone?: string | null, launchdarklyTrackingId: string, launchdarklyTrackingGroup: string, launchdarklyTrackingSubgroup?: string | null, launchdarklyTrackingTeam?: Array | null, launchdarklyTrackingTenant: string, launchdarklyTrackingCloudEnvironment: string, defaultProjectKey?: string | null, businessRole?: string | null, projects: { __typename?: 'ProjectQueryResult', total: number, results: Array<{ __typename?: 'Project', name: string, key: string, suspension: { __typename?: 'ProjectSuspension', isActive: boolean }, expiry: { __typename?: 'ProjectExpiry', isActive: boolean } }> }, idTokenUserInfo?: { __typename?: 'IdTokenUserInfo', iss: string, sub: string, aud: string, exp: number, iat?: number | null, email?: string | null, name?: string | null, additionalClaims?: string | null } | null } | null }; +export type TFetchLoggedInUserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email: string, createdAt: string, gravatarHash: string, firstName: string, lastName: string, language: string, numberFormat: string, timeZone?: string | null, launchdarklyTrackingId: string, launchdarklyTrackingGroup: string, launchdarklyTrackingSubgroup?: string | null, launchdarklyTrackingTeam?: Array | null, launchdarklyTrackingTenant: string, launchdarklyTrackingCloudEnvironment: string, defaultProjectKey?: string | null, businessRole?: string | null, projects: { __typename?: 'ProjectQueryResult', total: number, results: Array<{ __typename?: 'Project', name: string, key: string, isProductionProject: boolean, suspension: { __typename?: 'ProjectSuspension', isActive: boolean }, expiry: { __typename?: 'ProjectExpiry', isActive: boolean } }> }, idTokenUserInfo?: { __typename?: 'IdTokenUserInfo', iss: string, sub: string, aud: string, exp: number, iat?: number | null, email?: string | null, name?: string | null, additionalClaims?: string | null } | null } | null }; export type TFetchUserProjectsQueryVariables = Exact<{ [key: string]: never; }>; diff --git a/packages/i18n/README.md b/packages/i18n/README.md index 667e2bc5a3..1d97cba266 100644 --- a/packages/i18n/README.md +++ b/packages/i18n/README.md @@ -146,7 +146,7 @@ const Application = (props) => { After you have defined the `intl` messages in your React components, you should extract those messages into the source file `core.json`. This file contains a key-value map of the message `id` and the message value. -To extract the messages simply run `mc-scripts extract-intl [options]`. +To extract the messages simply run `pnpm extract-intl`. ## Syncing translations with Transifex diff --git a/packages/i18n/data/core.json b/packages/i18n/data/core.json index 0254d9cfde..1591ef6820 100644 --- a/packages/i18n/data/core.json +++ b/packages/i18n/data/core.json @@ -53,7 +53,9 @@ "ErrorApologizer.title": "Sorry! An unexpected error occured.", "FailedAuthentication.paragraph1": "Please try again and contact your system administrator if you have any further questions.", "FailedAuthentication.title": "We are unable to authorize you to use the Merchant Center.", - "LocaleSwitcher.localesLabel": "Locales menu", + "LocaleSwitcher.dialogLocaleDescription": "The selected data locale will serve as the default setting for all localized fields within the Merchant Center, including names, descriptions, and other localized attributes. It's important to note that this selection does not affect the interface language of the Merchant Center or any data formatting options. To modify these settings, navigate to your user profile.", + "LocaleSwitcher.dialogLocaleTitle": "Selecting a data locale", + "LocaleSwitcher.localesLabel": "Locales", "NavBar.MCSupport.title": "Support", "Notification.hideNotification": "Hide notification", "PageNotFound.paragraph1": "The item you are looking for may have been deleted, does not exist, or the URL was entered incorrectly. Check the URL and try again. Please contact your system administrator or the commercetools Help Desk if you have any further questions.", @@ -69,12 +71,16 @@ "ProjectNotFound.title": "We could not find this Project", "ProjectNotInitialized.paragraph1": "Initialization should not take longer than a few minutes. Please contact us at {mailto} in case the project does not get initialized.", "ProjectNotInitialized.title": "Your project has not yet been initialized", + "ProjectStamp.expired": "Trial expired", + "ProjectStamp.production": "Production", + "ProjectStamp.suspended": "Suspended", + "ProjectStamp.willExpire": "{daysLeft, select, 0 {Trial ends today} 1 {Trial ends in 1 day} other {Trial ends in {daysLeft} days}}", "ProjectSuspended.defaultSuspensionMessage": "Your Project has been suspended", "ProjectSuspended.paragraph1": "Please contact your system administrator if you have any further questions or select another Project to view.", "ProjectSuspended.temporaryMaintenanceSuspensionMessage": "Your Project is temporarily suspended due to maintenance.", "ProjectSwitcher.expired": "Expired", "ProjectSwitcher.noResults": "Sorry, but there are no projects that match your search.", - "ProjectSwitcher.projectsLabel": "Projects menu", + "ProjectSwitcher.projectsLabel": "Projects", "ProjectSwitcher.searchPlaceholder": "Search for a project", "ProjectSwitcher.suspended": "Suspended", "QuickAccess.inputPlaceholder": "Go to...", diff --git a/packages/i18n/data/en.json b/packages/i18n/data/en.json index f4e196e84e..160a76aa59 100644 --- a/packages/i18n/data/en.json +++ b/packages/i18n/data/en.json @@ -53,7 +53,7 @@ "ErrorApologizer.title": "Sorry! An unexpected error occured.", "FailedAuthentication.paragraph1": "Please try again or contact your system administrator if you have any further questions.", "FailedAuthentication.title": "We are unable to authorize you to use the Merchant Center.", - "LocaleSwitcher.localesLabel": "Locales menu", + "LocaleSwitcher.localesLabel": "Locales", "NavBar.MCSupport.title": "Support", "Notification.hideNotification": "Hide notification", "PageNotFound.paragraph1": "The item you are looking for may have been deleted, does not exist, or the URL was entered incorrectly. Check the URL and try again. Please contact your system administrator or the commercetools Help Desk if you have any further questions.", @@ -74,7 +74,7 @@ "ProjectSuspended.temporaryMaintenanceSuspensionMessage": "Your Project is temporarily suspended due to maintenance.", "ProjectSwitcher.expired": "Expired", "ProjectSwitcher.noResults": "Sorry, but there are no projects that match your search.", - "ProjectSwitcher.projectsLabel": "Projects menu", + "ProjectSwitcher.projectsLabel": "Projects", "ProjectSwitcher.searchPlaceholder": "Search for a project", "ProjectSwitcher.suspended": "Suspended", "QuickAccess.inputPlaceholder": "Go to...", diff --git a/packages/permissions/src/components/branch-on-permissions/branch-on-permissions.spec.tsx b/packages/permissions/src/components/branch-on-permissions/branch-on-permissions.spec.tsx index 0f73317d2c..33b28d574f 100644 --- a/packages/permissions/src/components/branch-on-permissions/branch-on-permissions.spec.tsx +++ b/packages/permissions/src/components/branch-on-permissions/branch-on-permissions.spec.tsx @@ -31,6 +31,7 @@ const renderWithPermissions = (demandedPermissions: string[]) => { name: 'P1 ', expiry: { isActive: false }, suspension: { isActive: false }, + isProductionProject: false, }, ], }, diff --git a/packages/permissions/src/types/generated/mc.ts b/packages/permissions/src/types/generated/mc.ts index 1dd175d231..3c6a76a513 100644 --- a/packages/permissions/src/types/generated/mc.ts +++ b/packages/permissions/src/types/generated/mc.ts @@ -425,6 +425,7 @@ export type TQuery = { release?: Maybe; releases?: Maybe; storeOAuthScopes: Array; + systemStatus: TSystemStatus; }; @@ -591,6 +592,17 @@ export type TSupportedStoreScope = { name: Scalars['String']; }; +export enum TSystemOperabilityStatus { + Degraded = 'DEGRADED', + Operational = 'OPERATIONAL', + Outage = 'OUTAGE' +} + +export type TSystemStatus = { + __typename?: 'SystemStatus'; + status: TSystemOperabilityStatus; +}; + export type TUser = TMetaData & { __typename?: 'User'; businessRole?: Maybe; @@ -659,7 +671,7 @@ export type TFetchProjectQuery = { __typename?: 'Query', project?: { __typename? export type TFetchLoggedInUserQueryVariables = Exact<{ [key: string]: never; }>; -export type TFetchLoggedInUserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email: string, createdAt: string, gravatarHash: string, firstName: string, lastName: string, language: string, numberFormat: string, timeZone?: string | null, launchdarklyTrackingId: string, launchdarklyTrackingGroup: string, launchdarklyTrackingSubgroup?: string | null, launchdarklyTrackingTeam?: Array | null, launchdarklyTrackingTenant: string, launchdarklyTrackingCloudEnvironment: string, defaultProjectKey?: string | null, businessRole?: string | null, projects: { __typename?: 'ProjectQueryResult', total: number, results: Array<{ __typename?: 'Project', name: string, key: string, suspension: { __typename?: 'ProjectSuspension', isActive: boolean }, expiry: { __typename?: 'ProjectExpiry', isActive: boolean } }> }, idTokenUserInfo?: { __typename?: 'IdTokenUserInfo', iss: string, sub: string, aud: string, exp: number, iat?: number | null, email?: string | null, name?: string | null, additionalClaims?: string | null } | null } | null }; +export type TFetchLoggedInUserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email: string, createdAt: string, gravatarHash: string, firstName: string, lastName: string, language: string, numberFormat: string, timeZone?: string | null, launchdarklyTrackingId: string, launchdarklyTrackingGroup: string, launchdarklyTrackingSubgroup?: string | null, launchdarklyTrackingTeam?: Array | null, launchdarklyTrackingTenant: string, launchdarklyTrackingCloudEnvironment: string, defaultProjectKey?: string | null, businessRole?: string | null, projects: { __typename?: 'ProjectQueryResult', total: number, results: Array<{ __typename?: 'Project', name: string, key: string, isProductionProject: boolean, suspension: { __typename?: 'ProjectSuspension', isActive: boolean }, expiry: { __typename?: 'ProjectExpiry', isActive: boolean } }> }, idTokenUserInfo?: { __typename?: 'IdTokenUserInfo', iss: string, sub: string, aud: string, exp: number, iat?: number | null, email?: string | null, name?: string | null, additionalClaims?: string | null } | null } | null }; export type TFetchUserProjectsQueryVariables = Exact<{ [key: string]: never; }>; diff --git a/playground/src/i18n/data/core.json b/playground/src/i18n/data/core.json index c6af2fe3ce..f2f69522fe 100644 --- a/playground/src/i18n/data/core.json +++ b/playground/src/i18n/data/core.json @@ -1,24 +1,21 @@ { "Menu.StateMachines": "State Machines", "Menu.EchoServer": "Echo Server", - "LocaleSwitcher.localesLabel": "Locales menu", - "ProjectSwitcher.projectsLabel": "Projects menu", - "UserSettingsMenu.menuLabel": "User settings menu", "StateMachines.EchoServer.description": "This page demonstrates how to connect a Custom Application to an external API, using the \"/proxy/forward-to\" endpoint. For demo purposes, the external API used by this page is a simple echo server, which just returns some information about the request sent.", "StateMachines.EchoServer.labelIncludeForwardHeaderInRequest": "Inlude Forward Header in request", "StateMachines.EchoServer.labelIncludeParamsInRequest": "Inlude Parameter in request", "StateMachines.EchoServer.labelSendRequest": "Send request", "StateMachines.EchoServer.labelSending": "Sending...", "StateMachines.EchoServer.title": "Echo Server", + "StateMachines.FormattersDemo.dateSelectorLabel": "Date selector:", + "StateMachines.FormattersDemo.fullDateLabel": "Full date:", + "StateMachines.FormattersDemo.localeLabel": "Current locale:", + "StateMachines.FormattersDemo.moneyLabel": "Money:", + "StateMachines.FormattersDemo.subtitle": "You can see here formatted dates and numbers with different locales.", + "StateMachines.FormattersDemo.title": "Formatters Demo", "StateMachines.ListView.column.stateMachineKey": "Key", "StateMachines.ListView.column.stateMachineName": "Name", "StateMachines.ListView.noResults": "There are no results matching your search criteria.", "StateMachines.ListView.objectsInCache": "There are {count} objects in the cache.", - "StateMachines.ListView.title": "State Machines", - "StateMachines.FormattersDemo.title": "Formatters Demo", - "StateMachines.FormattersDemo.subtitle": "You can see here formatted dates and numbers with different locales.", - "StateMachines.FormattersDemo.localeLabel": "Current locale:", - "StateMachines.FormattersDemo.fullDateLabel": "Full date:", - "StateMachines.FormattersDemo.dateSelectorLabel": "Date selector:", - "StateMachines.FormattersDemo.moneyLabel": "Money:" + "StateMachines.ListView.title": "State Machines" } diff --git a/playground/src/i18n/data/de.json b/playground/src/i18n/data/de.json index c6af2fe3ce..0aac582cca 100644 --- a/playground/src/i18n/data/de.json +++ b/playground/src/i18n/data/de.json @@ -1,8 +1,6 @@ { "Menu.StateMachines": "State Machines", "Menu.EchoServer": "Echo Server", - "LocaleSwitcher.localesLabel": "Locales menu", - "ProjectSwitcher.projectsLabel": "Projects menu", "UserSettingsMenu.menuLabel": "User settings menu", "StateMachines.EchoServer.description": "This page demonstrates how to connect a Custom Application to an external API, using the \"/proxy/forward-to\" endpoint. For demo purposes, the external API used by this page is a simple echo server, which just returns some information about the request sent.", "StateMachines.EchoServer.labelIncludeForwardHeaderInRequest": "Inlude Forward Header in request", diff --git a/playground/src/i18n/data/en.json b/playground/src/i18n/data/en.json index c6af2fe3ce..f15aae6429 100644 --- a/playground/src/i18n/data/en.json +++ b/playground/src/i18n/data/en.json @@ -1,8 +1,8 @@ { "Menu.StateMachines": "State Machines", "Menu.EchoServer": "Echo Server", - "LocaleSwitcher.localesLabel": "Locales menu", - "ProjectSwitcher.projectsLabel": "Projects menu", + "LocaleSwitcher.localesLabel": "Locales", + "ProjectSwitcher.projectsLabel": "Projects", "UserSettingsMenu.menuLabel": "User settings menu", "StateMachines.EchoServer.description": "This page demonstrates how to connect a Custom Application to an external API, using the \"/proxy/forward-to\" endpoint. For demo purposes, the external API used by this page is a simple echo server, which just returns some information about the request sent.", "StateMachines.EchoServer.labelIncludeForwardHeaderInRequest": "Inlude Forward Header in request", diff --git a/playground/src/i18n/data/es.json b/playground/src/i18n/data/es.json index c6af2fe3ce..f15aae6429 100644 --- a/playground/src/i18n/data/es.json +++ b/playground/src/i18n/data/es.json @@ -1,8 +1,8 @@ { "Menu.StateMachines": "State Machines", "Menu.EchoServer": "Echo Server", - "LocaleSwitcher.localesLabel": "Locales menu", - "ProjectSwitcher.projectsLabel": "Projects menu", + "LocaleSwitcher.localesLabel": "Locales", + "ProjectSwitcher.projectsLabel": "Projects", "UserSettingsMenu.menuLabel": "User settings menu", "StateMachines.EchoServer.description": "This page demonstrates how to connect a Custom Application to an external API, using the \"/proxy/forward-to\" endpoint. For demo purposes, the external API used by this page is a simple echo server, which just returns some information about the request sent.", "StateMachines.EchoServer.labelIncludeForwardHeaderInRequest": "Inlude Forward Header in request", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 887475e5e6..747c6c8f57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1426,6 +1426,9 @@ importers: '@commercetools-uikit/spacings': specifier: ^18.1.0 version: 18.1.0(@types/react@17.0.56)(react-dom@17.0.2)(react@17.0.2) + '@commercetools-uikit/stamp': + specifier: ^18.1.0 + version: 18.1.0(@types/react@17.0.56)(react-dom@17.0.2)(react-intl@6.4.5)(react@17.0.2) '@commercetools-uikit/text': specifier: ^18.1.0 version: 18.1.0(@types/react@17.0.56)(react-dom@17.0.2)(react-intl@6.4.5)(react@17.0.2) @@ -1659,6 +1662,9 @@ importers: '@commercetools-uikit/flat-button': specifier: ^18.1.0 version: 18.1.0(@types/react@17.0.56)(react-dom@17.0.2)(react@17.0.2)(typescript@5.2.2) + '@commercetools-uikit/icon-button': + specifier: ^18.1.0 + version: 18.1.0(@types/react@17.0.56)(react-dom@17.0.2)(react@17.0.2)(typescript@5.2.2) '@commercetools-uikit/icons': specifier: ^18.1.0 version: 18.1.0(@types/react@17.0.56)(react-dom@17.0.2)(react@17.0.2) @@ -6970,16 +6976,16 @@ packages: shelljs: 0.8.5 dev: false - /@commercetools-frontend/application-config@22.17.1: - resolution: {integrity: sha512-XmXYrmRXarz6jnyrEz+12uXQtCChZtA5T8c9EiprtTjT7oXHFquZwf1y2BSz5L8mbQ8Qrc5rjoN//yQTq8sCAw==} + /@commercetools-frontend/application-config@22.17.2: + resolution: {integrity: sha512-SKzW1V668UX/JtkFtT2ueysfM74lqzTt7t+BZBMn5HnX3rq9sNSae+yHh3z1OrVBgHso8sHRpzMfl58rSSv/ow==} engines: {node: 16.x || >=18.0.0} dependencies: '@babel/core': 7.23.0 '@babel/register': 7.22.15(@babel/core@7.23.0) '@babel/runtime': 7.23.2 '@babel/runtime-corejs3': 7.22.15 - '@commercetools-frontend/babel-preset-mc-app': 22.17.1 - '@commercetools-frontend/constants': 22.17.1 + '@commercetools-frontend/babel-preset-mc-app': 22.17.2 + '@commercetools-frontend/constants': 22.17.2 '@types/dompurify': 2.4.0 '@types/lodash': 4.14.198 '@types/react': 17.0.56 @@ -6997,8 +7003,8 @@ packages: - utf-8-validate dev: true - /@commercetools-frontend/babel-preset-mc-app@22.17.1: - resolution: {integrity: sha512-kzGDO+xbR7GuCuDlG9z5oa1jw9nBTJj57wjqSWTGDu8FxdDSBkw3RvVhVx/n4peaL9FBRl66dirEG7b3r3pE/A==} + /@commercetools-frontend/babel-preset-mc-app@22.17.2: + resolution: {integrity: sha512-UXKBsFvtkX1X4r4fmpr+elimroj+ZxSD9cPkMGwb4KT0ehklkkiqEkBe1PBjtBKr/tAwdUqRGi8FnPVaRRlaxg==} engines: {node: 16.x || >=18.0.0} dependencies: '@babel/core': 7.23.0 @@ -7030,8 +7036,8 @@ packages: - supports-color dev: true - /@commercetools-frontend/constants@22.17.1: - resolution: {integrity: sha512-1ly/uW6G0dOYrSEZEdWJ9zSqiWVLhzX39JvDURGWLZ2XigH+OUlobJFexDPpkCmkimnBvLy4hVcydF5GoItsqA==} + /@commercetools-frontend/constants@22.17.2: + resolution: {integrity: sha512-UDp+KjwCNfXbfyVaga8I6pq7zGykXtuOkKj8fdHLBCJvUU/YZ+vzzyPedn4PBERRHQuqc1uJgPwiqVfiV2p4ng==} dependencies: '@babel/runtime': 7.23.2 '@babel/runtime-corejs3': 7.22.15 @@ -7104,8 +7110,8 @@ packages: dependencies: '@babel/runtime': 7.23.2 '@babel/runtime-corejs3': 7.22.15 - '@commercetools-frontend/application-config': 22.17.1 - '@commercetools-frontend/constants': 22.17.1 + '@commercetools-frontend/application-config': 22.17.2 + '@commercetools-frontend/constants': 22.17.2 '@commercetools-test-data/commons': 6.4.1 '@commercetools-test-data/core': 6.4.1 '@commercetools-test-data/utils': 6.4.1 @@ -9707,6 +9713,26 @@ packages: - react-intl dev: false + /@commercetools-uikit/stamp@18.1.0(@types/react@17.0.56)(react-dom@17.0.2)(react-intl@6.4.5)(react@17.0.2): + resolution: {integrity: sha512-MPLVL+asW5pKogH8gr/4PofcahxrjzfjCEdmt6wVLyFbunoe+XK9EqqiEEFtlO19hcHS63OcOsr3XJmB//MZ6Q==} + peerDependencies: + react: 17.x + dependencies: + '@babel/runtime': 7.23.2 + '@babel/runtime-corejs3': 7.22.15 + '@commercetools-uikit/design-system': 18.1.0(@types/react@17.0.56)(react-dom@17.0.2) + '@commercetools-uikit/spacings-inline': 18.1.0(@types/react@17.0.56)(react-dom@17.0.2)(react@17.0.2) + '@commercetools-uikit/text': 18.1.0(@types/react@17.0.56)(react-dom@17.0.2)(react-intl@6.4.5)(react@17.0.2) + '@commercetools-uikit/utils': 18.1.0(react@17.0.2) + '@emotion/react': 11.11.1(@types/react@17.0.56)(react@17.0.2) + prop-types: 15.8.1 + react: 17.0.2 + transitivePeerDependencies: + - '@types/react' + - react-dom + - react-intl + dev: false + /@commercetools-uikit/text-field@16.12.1(@types/react@17.0.56)(react-dom@18.2.0)(react@18.2.0)(typescript@5.2.2): resolution: {integrity: sha512-vg5H0YKKaSfr4Af/yzKCIFLhN7hiBt+0amo/L3o/dQhwSha7L2z7S3kcrN2OVnxGVS/dbk1UiLu/a/vF8EnvMw==} peerDependencies: @@ -19387,7 +19413,7 @@ packages: peerDependencies: react: '>=16.12.0' dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.2 compute-scroll-into-view: 1.0.20 prop-types: 15.8.1 react: 18.2.0 @@ -34413,7 +34439,3 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: false - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/schemas/mc.json b/schemas/mc.json index 184e898533..25ca4898eb 100644 --- a/schemas/mc.json +++ b/schemas/mc.json @@ -3909,6 +3909,22 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "systemStatus", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SystemStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -4876,6 +4892,62 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "SystemOperabilityStatus", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "DEGRADED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OPERATIONAL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OUTAGE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SystemStatus", + "description": null, + "fields": [ + { + "name": "status", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "SystemOperabilityStatus", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "User", diff --git a/test-data/types/generated/mc.ts b/test-data/types/generated/mc.ts index 1dd175d231..3c6a76a513 100644 --- a/test-data/types/generated/mc.ts +++ b/test-data/types/generated/mc.ts @@ -425,6 +425,7 @@ export type TQuery = { release?: Maybe; releases?: Maybe; storeOAuthScopes: Array; + systemStatus: TSystemStatus; }; @@ -591,6 +592,17 @@ export type TSupportedStoreScope = { name: Scalars['String']; }; +export enum TSystemOperabilityStatus { + Degraded = 'DEGRADED', + Operational = 'OPERATIONAL', + Outage = 'OUTAGE' +} + +export type TSystemStatus = { + __typename?: 'SystemStatus'; + status: TSystemOperabilityStatus; +}; + export type TUser = TMetaData & { __typename?: 'User'; businessRole?: Maybe; @@ -659,7 +671,7 @@ export type TFetchProjectQuery = { __typename?: 'Query', project?: { __typename? export type TFetchLoggedInUserQueryVariables = Exact<{ [key: string]: never; }>; -export type TFetchLoggedInUserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email: string, createdAt: string, gravatarHash: string, firstName: string, lastName: string, language: string, numberFormat: string, timeZone?: string | null, launchdarklyTrackingId: string, launchdarklyTrackingGroup: string, launchdarklyTrackingSubgroup?: string | null, launchdarklyTrackingTeam?: Array | null, launchdarklyTrackingTenant: string, launchdarklyTrackingCloudEnvironment: string, defaultProjectKey?: string | null, businessRole?: string | null, projects: { __typename?: 'ProjectQueryResult', total: number, results: Array<{ __typename?: 'Project', name: string, key: string, suspension: { __typename?: 'ProjectSuspension', isActive: boolean }, expiry: { __typename?: 'ProjectExpiry', isActive: boolean } }> }, idTokenUserInfo?: { __typename?: 'IdTokenUserInfo', iss: string, sub: string, aud: string, exp: number, iat?: number | null, email?: string | null, name?: string | null, additionalClaims?: string | null } | null } | null }; +export type TFetchLoggedInUserQuery = { __typename?: 'Query', user?: { __typename?: 'User', id: string, email: string, createdAt: string, gravatarHash: string, firstName: string, lastName: string, language: string, numberFormat: string, timeZone?: string | null, launchdarklyTrackingId: string, launchdarklyTrackingGroup: string, launchdarklyTrackingSubgroup?: string | null, launchdarklyTrackingTeam?: Array | null, launchdarklyTrackingTenant: string, launchdarklyTrackingCloudEnvironment: string, defaultProjectKey?: string | null, businessRole?: string | null, projects: { __typename?: 'ProjectQueryResult', total: number, results: Array<{ __typename?: 'Project', name: string, key: string, isProductionProject: boolean, suspension: { __typename?: 'ProjectSuspension', isActive: boolean }, expiry: { __typename?: 'ProjectExpiry', isActive: boolean } }> }, idTokenUserInfo?: { __typename?: 'IdTokenUserInfo', iss: string, sub: string, aud: string, exp: number, iat?: number | null, email?: string | null, name?: string | null, additionalClaims?: string | null } | null } | null }; export type TFetchUserProjectsQueryVariables = Exact<{ [key: string]: never; }>; diff --git a/visual-testing-app/src/components/project-stamp/project-stamp.visualroute.tsx b/visual-testing-app/src/components/project-stamp/project-stamp.visualroute.tsx new file mode 100644 index 0000000000..e17356c4c3 --- /dev/null +++ b/visual-testing-app/src/components/project-stamp/project-stamp.visualroute.tsx @@ -0,0 +1,21 @@ +import { ProjectStamp } from '@commercetools-frontend/application-components'; +import { Suite, Spec } from '../../test-utils'; + +export const routePath = '/project-stamp'; + +export const Component = () => ( + + + + + + + + + + + + + + +); diff --git a/visual-testing-app/src/components/project-stamp/project-stamp.visualspec.ts b/visual-testing-app/src/components/project-stamp/project-stamp.visualspec.ts new file mode 100644 index 0000000000..f784bd775d --- /dev/null +++ b/visual-testing-app/src/components/project-stamp/project-stamp.visualspec.ts @@ -0,0 +1,13 @@ +import percySnapshot from '@percy/puppeteer'; +import { HOST } from '../../constants'; + +describe('ProjectStamp', () => { + beforeAll(async () => { + await page.goto(`${HOST}/project-stamp`); + }); + + it('Default', async () => { + await page.waitForSelector('text/Production project stamp'); + await percySnapshot(page, 'ProjectStamp'); + }); +});