Application Marketplace available in dashboard (#2054)

* Add marketplace

* Update messages

* Extract styles

* Update test env

* Update config and rename uri

* Update template

* Trigger CI

* Possible fix

* Update deploy yml

* Add marketplace to staging

* Fix responsiveness

* Trigger CI

* Fix navigation tests (#2081)

* fix navigate through shop

* wait for progress bar not to exist

* simplify code

* add wait

* Fix navigate through shop (#2076)

* fix navigate through shop

* wait for progress bar not to exist

* simplify code

* Refine App about section (#2056)

* Trigger CI

* Trigger CI

* Trigger CI

* Trigger CI

* Fix app activation/deactivation

* Hide configuration url and policy if theres none

* Remove about and support links

* Remove unused code

* Update stories and tests

* Update messages

* Fix privacy policy

* Fix activation and "open app"

* Update tests

Co-authored-by: Karolina Rakoczy <rakoczy.karolina@gmail.com>
This commit is contained in:
Wojciech Mista 2022-06-08 08:44:28 +02:00 committed by GitHub
parent a8b584a9d6
commit c922dfb6fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 201 additions and 174 deletions

View file

@ -25,3 +25,4 @@ greatly reduce the amount of work needed to review your work. -->
Modify API_URI if you want test instance to use custom backend. CYPRESS_API_URI is optional, use when necessary. -->
API_URI=https://automation-dashboard.staging.saleor.cloud/graphql/
MARKETPLACE_URL=https://marketplace-gray.vercel.app/

View file

@ -10,6 +10,7 @@ jobs:
runs-on: ubuntu-20.04
env:
API_URI: /graphql/
MARKETPLACE_URL: "https://marketplace-gray.vercel.app/"
APP_MOUNT_URI: /dashboard/
STATIC_URL: /dashboard/static/
SENTRY_ORG: saleor

View file

@ -57,11 +57,21 @@ jobs:
pattern: (http|https)://[a-zA-Z0-9.-]+/graphql/?
run: |
echo "::set-output name=custom_api_uri::$(echo $pull_request_body | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1)"
- name: Get MARKETPLACE_URL
id: marketplace_url
# Search for MARKETPLACE_URL in PR description
env:
pull_request_body: ${{ github.event.pull_request.body }}
prefix: MARKETPLACE_URL=
pattern: (http|https)://[a-zA-Z0-9.-]+/?
run: |
echo "::set-output name=custom_marketplace_url::$(echo $pull_request_body | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1)"
- name: Run build
env:
# Use custom API_URI or the default one
API_URI: ${{ steps.api_uri.outputs.custom_api_uri || 'https://qa.staging.saleor.cloud/graphql/' }}
MARKETPLACE_URL: ${{ steps.marketplace_url.outputs.custom_marketplace_url }}
APP_MOUNT_URI: /
STATIC_URL: /
IS_CLOUD_INSTANCE: true
@ -113,7 +123,6 @@ jobs:
env_url: https://${{ steps.set-domain.outputs.domain }}/storybook/index.html
deployment_id: ${{ steps.storybook-deployment.outputs.deployment_id }}
cypress-run-critical:
needs: deploy
runs-on: ubuntu-latest

View file

@ -5,11 +5,12 @@ RUN npm install
COPY . .
ARG APP_MOUNT_URI
ARG API_URI
ARG MARKETPLACE_URL
ARG STATIC_URL
ENV API_URI ${API_URI:-http://localhost:8000/graphql/}
ENV APP_MOUNT_URI ${APP_MOUNT_URI:-/dashboard/}
ENV STATIC_URL ${STATIC_URL:-/dashboard/}
RUN STATIC_URL=${STATIC_URL} API_URI=${API_URI} APP_MOUNT_URI=${APP_MOUNT_URI} npm run build
RUN STATIC_URL=${STATIC_URL} API_URI=${API_URI} MARKETPLACE_URL=${MARKETPLACE_URL} APP_MOUNT_URI=${APP_MOUNT_URI} npm run build
FROM nginx:stable
WORKDIR /app

View file

@ -5,6 +5,7 @@ RUN npm install
COPY . .
ARG APP_MOUNT_URI
ARG API_URI
ARG MARKETPLACE_URL
ARG STATIC_URL
ENV API_URI ${API_URI:-http://localhost:8000/graphql/}
ENV APP_MOUNT_URI ${APP_MOUNT_URI:-/}

View file

@ -5,6 +5,7 @@ export const LEFT_MENU_SELECTORS = {
home: "[data-test='menu-item-label'][data-test-id='home']",
orders: "[data-test='menu-item-label'][data-test-id='orders']",
discounts: "[data-test='menu-item-label'][data-test-id='discounts']",
appSection: "[data-test='menu-item-label'][data-test-id='apps_section']",
app: "[data-test='menu-item-label'][data-test-id='apps']",
translations: "[data-test='menu-item-label'][data-test-id='translations']",
customers: "[data-test='menu-item-label'][data-test-id='customers']"
@ -22,3 +23,9 @@ export const CATALOG = {
categories: "[data-test='submenu-item-label'][data-test-id='categories']",
collections: "[data-test='submenu-item-label'][data-test-id='collections']"
};
export const APP_MENU_SELECTORS = {
app: "[data-test='submenu-item-label'][data-test-id='apps']"
};
export const appCommonSelector = "[data-test-id*='apps']";

View file

@ -7,9 +7,6 @@ const configurationAsParent = {
};
export const PERMISSIONS = {
app: {
permissionSelectors: [menuSelectors.LEFT_MENU_SELECTORS.app]
},
channel: {
parent: configurationAsParent,
permissionSelectors: [CONFIGURATION_SELECTORS.channels]

View file

@ -9,7 +9,7 @@ export const PERMISSIONS_OPTIONS = {
},
app: {
user: ONE_PERMISSION_USERS.app,
permissions: [PERMISSIONS.app],
permissions: [],
testCase: "TC: SALEOR_3402"
},
channel: {

View file

@ -1,17 +1,55 @@
/// <reference types="cypress"/>
/// <reference types="../support"/>
import {
APP_MENU_SELECTORS,
appCommonSelector,
LEFT_MENU_SELECTORS
} from "../elements/account/left-menu/left-menu-selectors";
import { PERMISSIONS_OPTIONS } from "../fixtures/permissionsUsers";
import filterTests from "../support/filterTests";
import * as permissionsSteps from "../support/pages/permissionsPage";
describe("As a staff user I want to navigate through shop using different permissions", () => {
Object.keys(PERMISSIONS_OPTIONS).forEach(key => {
const permissionsOptions = PERMISSIONS_OPTIONS;
before(() => {
cy.loginUserViaRequest()
.visit("/")
.get(appCommonSelector)
.should("be.visible")
.get("body")
.then($body => {
// This will be deleted when Marketplace is released
// Consider this solution as temporary
let appPermissions;
if ($body.find(LEFT_MENU_SELECTORS.appSection).length) {
appPermissions = {
parent: {
parentMenuSelector: LEFT_MENU_SELECTORS.appSection,
parentSelectors: [APP_MENU_SELECTORS]
},
permissionSelectors: [APP_MENU_SELECTORS.app]
};
} else {
appPermissions = {
permissionSelectors: [LEFT_MENU_SELECTORS.app]
};
}
permissionsOptions.all.permissions.push(appPermissions);
permissionsOptions.app.permissions = [appPermissions];
});
});
Object.keys(permissionsOptions).forEach(key => {
const tags =
key === "all" ? ["critical", "all", "refactored"] : ["all", "refactored"];
filterTests({ definedTags: tags }, () => {
it(`should be able to navigate through shop as a staff member using ${key} permission. ${PERMISSIONS_OPTIONS[key].testCase}`, () => {
const permissionOption = PERMISSIONS_OPTIONS[key];
it(`should be able to navigate through shop as a staff member using ${key} permission. ${permissionsOptions[key].testCase}`, () => {
const permissionOption = permissionsOptions[key];
const permissions = permissionOption.permissions;
cy.clearSessionData();
permissionsSteps.navigateToAllAvailablePageAndCheckIfDisplayed(

View file

@ -1218,10 +1218,6 @@
"context": "WarehouseSettings public stock label",
"string": "Public Stock"
},
"89PSdB": {
"context": "link",
"string": "Edit settings"
},
"8B8E+3": {
"context": "navigator placeholder",
"string": "Order Number"
@ -1929,6 +1925,10 @@
"context": "date time attribute type",
"string": "Date Time"
},
"E+M17x": {
"context": "marketplace section name",
"string": "Marketplace"
},
"E22x4H": {
"context": "no card defuned alert message",
"string": "You havent defined a gift card product!"
@ -5700,10 +5700,6 @@
"hX5PAb": {
"string": "No results found"
},
"hdcGSJ": {
"context": "button",
"string": "Support/FAQ"
},
"hkENym": {
"string": "Customer"
},
@ -6204,10 +6200,6 @@
"llBnr+": {
"string": "Warehouse Name"
},
"llC1q8": {
"context": "button",
"string": "App home page"
},
"lm9NSK": {
"context": "password reset, button",
"string": "Send an email with reset link"

View file

@ -9,7 +9,6 @@ const props: AppDetailsPageProps = {
data: appDetails,
loading: false,
navigateToApp: () => undefined,
navigateToAppSettings: () => undefined,
onAppActivateOpen: () => undefined,
onAppDeactivateOpen: () => undefined
};

View file

@ -16,7 +16,6 @@ import { FormattedMessage, useIntl } from "react-intl";
import ReactMarkdown from "react-markdown";
import activateIcon from "../../../../assets/images/activate-icon.svg";
import settingsIcon from "../../../../assets/images/settings-icon.svg";
import supportIcon from "../../../../assets/images/support-icon.svg";
import { useStyles } from "../../styles";
import DeactivatedText from "../DeactivatedText";
@ -25,7 +24,6 @@ export interface AppDetailsPageProps {
loading: boolean;
data: AppQuery["app"];
navigateToApp: () => void;
navigateToAppSettings: () => void;
onAppActivateOpen: () => void;
onAppDeactivateOpen: () => void;
}
@ -34,7 +32,6 @@ export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
data,
loading,
navigateToApp,
navigateToAppSettings,
onAppActivateOpen,
onAppDeactivateOpen
}) => {
@ -76,20 +73,6 @@ export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
description="link"
/>
</ExternalLink>
{data.configurationUrl && (
<ButtonBase
className={classes.headerLinkContainer}
disableRipple
onClick={navigateToAppSettings}
>
<SVG src={settingsIcon} />
<FormattedMessage
id="89PSdB"
defaultMessage="Edit settings"
description="link"
/>
</ButtonBase>
)}
<ButtonBase
className={classes.headerLinkContainer}
disableRipple
@ -163,18 +146,17 @@ export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
</Card>
<CardSpacer />
<Card>
<CardTitle
title={intl.formatMessage({
id: "a55zOn",
defaultMessage: "Data privacy",
description: "section header"
})}
/>
<CardContent>
{!loading ? (
<>
<Typography>{data?.dataPrivacy}</Typography>
{(loading || data?.dataPrivacyUrl) && (
<Card>
<CardTitle
title={intl.formatMessage({
id: "a55zOn",
defaultMessage: "Data privacy",
description: "section header"
})}
/>
<CardContent>
{!loading ? (
<ExternalLink
className={classes.linkContainer}
href={data?.dataPrivacyUrl}
@ -186,12 +168,12 @@ export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
description="app privacy policy link"
/>
</ExternalLink>
</>
) : (
<Skeleton />
)}
</CardContent>
</Card>
) : (
<Skeleton />
)}
</CardContent>
</Card>
)}
<CardSpacer />
</Container>
);

View file

@ -1,5 +1,6 @@
import useShop from "@saleor/hooks/useShop";
import { useTheme } from "@saleor/macaw-ui";
import clsx from "clsx";
import React from "react";
import urlJoin from "url-join";
@ -10,6 +11,7 @@ interface Props {
src: string;
appToken: string;
appId: string;
className?: string;
onLoad?(): void;
onError?(): void;
}
@ -20,6 +22,7 @@ export const AppFrame: React.FC<Props> = ({
src,
appToken,
appId,
className,
onLoad,
onError
}) => {
@ -55,7 +58,7 @@ export const AppFrame: React.FC<Props> = ({
src={urlJoin(src, `?domain=${shop.domain.host}&id=${appId}`)}
onError={onError}
onLoad={handleLoad}
className={classes.iframe}
className={clsx(classes.iframe, className)}
sandbox="allow-same-origin allow-forms allow-scripts"
/>
);

View file

@ -70,32 +70,6 @@ export const AppPage: React.FC<AppPageProps> = ({
description="button"
/>
</Button>
<Button
component="a"
href={data?.homepageUrl}
variant="primary"
data-tc="open-app"
target="_blank"
>
<FormattedMessage
id="llC1q8"
defaultMessage="App home page"
description="button"
/>
</Button>
<Button
component="a"
href={data?.supportUrl}
variant="primary"
data-tc="open-support"
target="_blank"
>
<FormattedMessage
id="hdcGSJ"
defaultMessage="Support/FAQ"
description="button"
/>
</Button>
</div>
</Grid>
<CardSpacer />

View file

@ -55,9 +55,6 @@ export const appInstallUrl = appInstallPath;
export const appDetailsUrl = (id: string, params?: AppDetailsUrlQueryParams) =>
appDetailsPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
export const appSettingsUrl = (id: string, params?: AppDetailsUrlQueryParams) =>
appSettingsPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
export const appUrl = (id: string, params?: AppDetailsUrlQueryParams) =>
appPath(encodeURIComponent(id)) + "?" + stringifyQs(params);
export const appDeepUrl = (

View file

@ -16,9 +16,9 @@ import AppActivateDialog from "../../components/AppActivateDialog";
import AppDeactivateDialog from "../../components/AppDeactivateDialog";
import AppDetailsPage from "../../components/AppDetailsPage";
import {
appDetailsUrl,
AppDetailsUrlDialog,
AppDetailsUrlQueryParams,
appSettingsUrl,
appsListPath,
appUrl
} from "../../urls";
@ -88,7 +88,7 @@ export const AppDetails: React.FC<AppDetailsProps> = ({ id, params }) => {
const [openModal, closeModal] = createDialogActionHandlers<
AppDetailsUrlDialog,
AppDetailsUrlQueryParams
>(navigate, params => appUrl(id, params), params);
>(navigate, params => appDetailsUrl(id, params), params);
const handleActivateConfirm = () => {
activateApp(mutationOpts);
@ -121,7 +121,6 @@ export const AppDetails: React.FC<AppDetailsProps> = ({ id, params }) => {
data={data?.app}
loading={loading}
navigateToApp={() => navigate(appUrl(id))}
navigateToAppSettings={() => navigate(appSettingsUrl(id))}
onAppActivateOpen={() => openModal("app-activate")}
onAppDeactivateOpen={() => openModal("app-deactivate")}
/>

View file

@ -11,12 +11,14 @@ import {
extensionMountPoints,
useExtensions
} from "@saleor/apps/useExtensions";
import { MARKETPLACE_URL } from "@saleor/config";
import { configurationMenuUrl } from "@saleor/configuration";
import { getConfigMenuItemsPermissions } from "@saleor/configuration/utils";
import { giftCardListUrl } from "@saleor/giftCards/urls";
import { PermissionEnum, UserFragment } from "@saleor/graphql";
import { commonMessages, sectionNames } from "@saleor/intl";
import { SidebarMenuItem } from "@saleor/macaw-ui";
import { marketplaceUrl } from "@saleor/marketplace/urls";
import { pageListPath } from "@saleor/pages/urls";
import { IntlShape } from "react-intl";
@ -55,6 +57,42 @@ function useMenuStructure(
label: intl.formatMessage(sectionNames.appExtensions)
};
// This will be deleted when Marketplace is released
// Consider this solution as temporary
const getAppSection = () => {
if (MARKETPLACE_URL) {
return {
ariaLabel: "apps_section",
iconSrc: appsIcon,
label: intl.formatMessage(sectionNames.apps),
permissions: [PermissionEnum.MANAGE_APPS],
id: "apps_section",
children: [
{
label: intl.formatMessage(sectionNames.apps),
id: "apps",
url: appsListPath
},
{
ariaLabel: "marketplace",
label: intl.formatMessage(sectionNames.marketplace),
id: "marketplace",
url: marketplaceUrl
}
]
};
}
return {
ariaLabel: "apps",
iconSrc: appsIcon,
label: intl.formatMessage(sectionNames.apps),
permissions: [PermissionEnum.MANAGE_APPS],
id: "apps",
url: appsListPath
};
};
const menuItems: FilterableMenuItem[] = [
{
ariaLabel: "home",
@ -202,14 +240,7 @@ function useMenuStructure(
id: "pages",
url: pageListPath
},
{
ariaLabel: "apps",
iconSrc: appsIcon,
label: intl.formatMessage(sectionNames.apps),
permissions: [PermissionEnum.MANAGE_APPS],
id: "apps",
url: appsListPath
},
getAppSection(),
{
ariaLabel: "translations",
children: extensions.NAVIGATION_TRANSLATIONS.length > 0 && [

View file

@ -8,6 +8,7 @@ export const APP_DEFAULT_URI = "/";
export const API_URI = process.env.API_URI;
export const SW_INTERVAL = parseInt(process.env.SW_INTERVAL, 10);
export const IS_CLOUD_INSTANCE = process.env.IS_CLOUD_INSTANCE === "true";
export const MARKETPLACE_URL = process.env.MARKETPLACE_URL;
export const DEFAULT_INITIAL_SEARCH_DATA: SearchVariables = {
after: null,

View file

@ -47,6 +47,8 @@ import { giftCardsSectionUrlName } from "./giftCards/urls";
import { apolloClient, saleorClient } from "./graphql/client";
import HomePage from "./home";
import { commonMessages } from "./intl";
import MarketplaceSection from "./marketplace";
import { marketplaceUrl } from "./marketplace/urls";
import NavigationSection from "./navigation";
import { navigationSection } from "./navigation/urls";
import { NotFound } from "./NotFound";
@ -250,6 +252,11 @@ const Routes: React.FC = () => {
path={appsSection}
component={AppsSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_APPS]}
path={marketplaceUrl}
component={MarketplaceSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path={warehouseSection}

View file

@ -382,6 +382,11 @@ export const sectionNames = defineMessages({
defaultMessage: "Home",
description: "home section name"
},
marketplace: {
id: "E+M17x",
defaultMessage: "Marketplace",
description: "marketplace section name"
},
navigation: {
id: "9C7PZE",
defaultMessage: "Navigation",

38
src/marketplace/index.tsx Normal file
View file

@ -0,0 +1,38 @@
import { Container } from "@material-ui/core";
import { AppFrame } from "@saleor/apps/components/AppFrame";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { MARKETPLACE_URL } from "@saleor/config";
import useNavigator from "@saleor/hooks/useNavigator";
import { sectionNames } from "@saleor/intl";
import React from "react";
import { useIntl } from "react-intl";
import { useStyles } from "./styles";
const Component = () => {
const classes = useStyles();
const intl = useIntl();
const navigate = useNavigator();
if (!MARKETPLACE_URL) {
return <NotFoundPage onBack={() => navigate("/")} />;
}
return (
<>
<WindowTitle title={intl.formatMessage(sectionNames.marketplace)} />
<Container>
<AppFrame
src={MARKETPLACE_URL}
// Marketplace doesn't require app token nor id
appToken=""
appId=""
className={classes.iframe}
/>
</Container>
</>
);
};
export default Component;

15
src/marketplace/styles.ts Normal file
View file

@ -0,0 +1,15 @@
import { makeStyles } from "@saleor/macaw-ui";
const useStyles = makeStyles(
() => ({
iframe: {
height: "100vh",
position: "sticky"
}
}),
{
name: "marketplaceStyles"
}
);
export { useStyles };

3
src/marketplace/urls.ts Normal file
View file

@ -0,0 +1,3 @@
const marketplaceSection = "/marketplace/";
export const marketplaceUrl = marketplaceSection;

View file

@ -24699,34 +24699,6 @@ exports[`Storyshots Views / Apps / App default 1`] = `
About
</span>
</button>
<a
aria-disabled="false"
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id"
data-tc="open-app"
href="http://localhost:8888/homepage"
tabindex="0"
target="_blank"
>
<span
class="MuiButton-label-id"
>
App home page
</span>
</a>
<a
aria-disabled="false"
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id"
data-tc="open-support"
href="http://localhost:8888/support"
tabindex="0"
target="_blank"
>
<span
class="MuiButton-label-id"
>
Support/FAQ
</span>
</a>
</div>
</div>
<div
@ -24806,13 +24778,6 @@ exports[`Storyshots Views / Apps / App details default 1`] = `
Get Support
</div>
</a>
<button
class="MuiButtonBase-root-id AppList-headerLinkContainer-id"
tabindex="0"
type="button"
>
Edit settings
</button>
<button
class="MuiButtonBase-root-id AppList-headerLinkContainer-id"
tabindex="0"
@ -24910,11 +24875,6 @@ exports[`Storyshots Views / Apps / App details default 1`] = `
<div
class="MuiCardContent-root-id"
>
<div
class="MuiTypography-root-id MuiTypography-body1-id"
>
Lorem ipsum
</div>
<a
class="ExternalLink-link-id"
href="http://localhost:8888/app-data-privacy"
@ -24993,13 +24953,6 @@ exports[`Storyshots Views / Apps / App details loading 1`] = `
Get Support
</div>
</a>
<button
class="MuiButtonBase-root-id AppList-headerLinkContainer-id"
tabindex="0"
type="button"
>
Edit settings
</button>
<button
class="MuiButtonBase-root-id AppList-headerLinkContainer-id"
tabindex="0"
@ -25144,34 +25097,6 @@ exports[`Storyshots Views / Apps / App settings 1`] = `
About
</span>
</button>
<a
aria-disabled="false"
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id"
data-tc="open-app"
href="http://localhost:8888/homepage"
tabindex="0"
target="_blank"
>
<span
class="MuiButton-label-id"
>
App home page
</span>
</a>
<a
aria-disabled="false"
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id"
data-tc="open-support"
href="http://localhost:8888/support"
tabindex="0"
target="_blank"
>
<span
class="MuiButton-label-id"
>
Support/FAQ
</span>
</a>
</div>
</div>
<div

View file

@ -38,6 +38,7 @@ const htmlWebpackPlugin = new HtmlWebpackPlugin({
});
const environmentPlugin = new webpack.EnvironmentPlugin({
API_URI: "",
MARKETPLACE_URL: "",
APP_MOUNT_URI: "/",
DEMO_MODE: false,
ENVIRONMENT: "",