Add dashboard extensions (#1292)
* Fix app embed * initial setup * Create ButtonWithSelect component * Use context * Use dialog * Handle app-bridge actions * Code adjustments, bump packages * Fix tests * Fix schema formatting * Send response * Use latest package versions
This commit is contained in:
parent
281bf7f613
commit
ab4c897b2a
32 changed files with 899 additions and 205 deletions
33
package-lock.json
generated
33
package-lock.json
generated
|
@ -69,6 +69,17 @@
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@apollo/react-testing": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apollo/react-testing/-/react-testing-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-1eKjN36UfIAnBVmfLbl12vQ/eCjTqYdaU95chGIQzT2uHd5BnasJu0z+MwXBrEs57A9WY9mFvLZxdjzQJXaacA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@apollo/react-common": "^3.1.4",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"tslib": "^1.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@apollographql/apollo-tools": {
|
"@apollographql/apollo-tools": {
|
||||||
"version": "0.4.9",
|
"version": "0.4.9",
|
||||||
"resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.4.9.tgz",
|
"resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.4.9.tgz",
|
||||||
|
@ -4181,9 +4192,27 @@
|
||||||
"picomatch": "^2.2.2"
|
"picomatch": "^2.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@saleor/app-bridge": {
|
||||||
|
"version": "0.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@saleor/app-bridge/-/app-bridge-0.1.5.tgz",
|
||||||
|
"integrity": "sha512-AkttPM/lLY1TOKjxoynKald659lb5+56NBwU22YEYWitguQDRR3zPrdti0KuzCkQ8CL99NeNo7JGr8T314kjVA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@saleor/macaw-ui": {
|
"@saleor/macaw-ui": {
|
||||||
"version": "github:mirumee/macaw-ui#03d63cd1342d2dc7fe18e9f8a9249ba27a52d9c2",
|
"version": "0.2.3",
|
||||||
"from": "github:mirumee/macaw-ui#03d63cd",
|
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.2.3.tgz",
|
||||||
|
"integrity": "sha512-9vcjY27lbxmySEnesJIefjjZAb/H8Hqm0AtCLUx6gZA2gvUkmJWQuYqcUljTY5VKEIuBpsG9BJtItRTWhQUG9g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.58",
|
"@material-ui/lab": "^4.0.0-alpha.58",
|
||||||
"@material-ui/styles": "^4.11.4",
|
"@material-ui/styles": "^4.11.4",
|
||||||
"@saleor/macaw-ui": "github:mirumee/macaw-ui#03d63cd",
|
"@saleor/macaw-ui": "^0.2.3",
|
||||||
"@sentry/react": "^6.0.0",
|
"@sentry/react": "^6.0.0",
|
||||||
"@types/faker": "^5.1.6",
|
"@types/faker": "^5.1.6",
|
||||||
"apollo": "^2.32.5",
|
"apollo": "^2.32.5",
|
||||||
|
@ -82,6 +82,7 @@
|
||||||
"use-react-router": "^1.0.7"
|
"use-react-router": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@apollo/react-testing": "^3.1.4",
|
||||||
"@babel/cli": "^7.5.5",
|
"@babel/cli": "^7.5.5",
|
||||||
"@babel/core": "^7.7.7",
|
"@babel/core": "^7.7.7",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.5.0",
|
"@babel/plugin-proposal-class-properties": "^7.5.0",
|
||||||
|
@ -99,6 +100,7 @@
|
||||||
"@pollyjs/core": "^5.0.0",
|
"@pollyjs/core": "^5.0.0",
|
||||||
"@pollyjs/persister-fs": "^5.0.0",
|
"@pollyjs/persister-fs": "^5.0.0",
|
||||||
"@release-it/bumper": "^2.0.0",
|
"@release-it/bumper": "^2.0.0",
|
||||||
|
"@saleor/app-bridge": "^0.1.5",
|
||||||
"@sentry/webpack-plugin": "^1.14.0",
|
"@sentry/webpack-plugin": "^1.14.0",
|
||||||
"@storybook/addon-storyshots": "^5.2.8",
|
"@storybook/addon-storyshots": "^5.2.8",
|
||||||
"@storybook/react": "^5.1.9",
|
"@storybook/react": "^5.1.9",
|
||||||
|
|
|
@ -222,6 +222,7 @@ type App implements Node & ObjectWithMetadata {
|
||||||
appUrl: String
|
appUrl: String
|
||||||
version: String
|
version: String
|
||||||
accessToken: String
|
accessToken: String
|
||||||
|
extensions: [AppExtension!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppActivate {
|
type AppActivate {
|
||||||
|
@ -289,6 +290,49 @@ enum AppErrorCode {
|
||||||
OUT_OF_SCOPE_PERMISSION
|
OUT_OF_SCOPE_PERMISSION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppExtension implements Node {
|
||||||
|
id: ID!
|
||||||
|
app: App!
|
||||||
|
label: String!
|
||||||
|
url: String!
|
||||||
|
view: AppExtensionViewEnum!
|
||||||
|
type: AppExtensionTypeEnum!
|
||||||
|
target: AppExtensionTargetEnum!
|
||||||
|
permissions: [Permission!]!
|
||||||
|
accessToken: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppExtensionCountableConnection {
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
edges: [AppExtensionCountableEdge!]!
|
||||||
|
totalCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppExtensionCountableEdge {
|
||||||
|
node: AppExtension!
|
||||||
|
cursor: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input AppExtensionFilterInput {
|
||||||
|
view: AppExtensionViewEnum
|
||||||
|
type: AppExtensionTypeEnum
|
||||||
|
target: AppExtensionTargetEnum
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AppExtensionTargetEnum {
|
||||||
|
MORE_ACTIONS
|
||||||
|
CREATE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AppExtensionTypeEnum {
|
||||||
|
OVERVIEW
|
||||||
|
DETAILS
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AppExtensionViewEnum {
|
||||||
|
PRODUCT
|
||||||
|
}
|
||||||
|
|
||||||
type AppFetchManifest {
|
type AppFetchManifest {
|
||||||
manifest: Manifest
|
manifest: Manifest
|
||||||
appErrors: [AppError!]! @deprecated(reason: "Use errors field instead. This field will be removed in Saleor 4.0.")
|
appErrors: [AppError!]! @deprecated(reason: "Use errors field instead. This field will be removed in Saleor 4.0.")
|
||||||
|
@ -329,6 +373,15 @@ type AppInstallation implements Node & Job {
|
||||||
message: String
|
message: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AppManifestExtension {
|
||||||
|
permissions: [Permission!]!
|
||||||
|
label: String!
|
||||||
|
url: String!
|
||||||
|
view: AppExtensionViewEnum!
|
||||||
|
type: AppExtensionTypeEnum!
|
||||||
|
target: AppExtensionTargetEnum!
|
||||||
|
}
|
||||||
|
|
||||||
type AppRetryInstall {
|
type AppRetryInstall {
|
||||||
appErrors: [AppError!]! @deprecated(reason: "Use errors field instead. This field will be removed in Saleor 4.0.")
|
appErrors: [AppError!]! @deprecated(reason: "Use errors field instead. This field will be removed in Saleor 4.0.")
|
||||||
errors: [AppError!]!
|
errors: [AppError!]!
|
||||||
|
@ -3243,6 +3296,7 @@ type Manifest {
|
||||||
dataPrivacyUrl: String
|
dataPrivacyUrl: String
|
||||||
homepageUrl: String
|
homepageUrl: String
|
||||||
supportUrl: String
|
supportUrl: String
|
||||||
|
extensions: [AppManifestExtension!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Margin {
|
type Margin {
|
||||||
|
@ -3633,7 +3687,7 @@ type Mutation {
|
||||||
draftOrderCreate(input: DraftOrderCreateInput!): DraftOrderCreate
|
draftOrderCreate(input: DraftOrderCreateInput!): DraftOrderCreate
|
||||||
draftOrderDelete(id: ID!): DraftOrderDelete
|
draftOrderDelete(id: ID!): DraftOrderDelete
|
||||||
draftOrderBulkDelete(ids: [ID]!): DraftOrderBulkDelete
|
draftOrderBulkDelete(ids: [ID]!): DraftOrderBulkDelete
|
||||||
draftOrderLinesBulkDelete(ids: [ID]!): DraftOrderLinesBulkDelete
|
draftOrderLinesBulkDelete(ids: [ID]!): DraftOrderLinesBulkDelete @deprecated(reason: "DEPRECATED: Will be removed in Saleor 4.0.")
|
||||||
draftOrderUpdate(id: ID!, input: DraftOrderInput!): DraftOrderUpdate
|
draftOrderUpdate(id: ID!, input: DraftOrderInput!): DraftOrderUpdate
|
||||||
orderAddNote(order: ID!, input: OrderAddNoteInput!): OrderAddNote
|
orderAddNote(order: ID!, input: OrderAddNoteInput!): OrderAddNote
|
||||||
orderCancel(id: ID!): OrderCancel
|
orderCancel(id: ID!): OrderCancel
|
||||||
|
@ -3655,7 +3709,7 @@ type Mutation {
|
||||||
orderMarkAsPaid(id: ID!, transactionReference: String): OrderMarkAsPaid
|
orderMarkAsPaid(id: ID!, transactionReference: String): OrderMarkAsPaid
|
||||||
orderRefund(amount: PositiveDecimal!, id: ID!): OrderRefund
|
orderRefund(amount: PositiveDecimal!, id: ID!): OrderRefund
|
||||||
orderUpdate(id: ID!, input: OrderUpdateInput!): OrderUpdate
|
orderUpdate(id: ID!, input: OrderUpdateInput!): OrderUpdate
|
||||||
orderUpdateShipping(order: ID!, input: OrderUpdateShippingInput): OrderUpdateShipping
|
orderUpdateShipping(order: ID!, input: OrderUpdateShippingInput!): OrderUpdateShipping
|
||||||
orderVoid(id: ID!): OrderVoid
|
orderVoid(id: ID!): OrderVoid
|
||||||
orderBulkCancel(ids: [ID]!): OrderBulkCancel
|
orderBulkCancel(ids: [ID]!): OrderBulkCancel
|
||||||
deleteMetadata(id: ID!, keys: [String!]!): DeleteMetadata
|
deleteMetadata(id: ID!, keys: [String!]!): DeleteMetadata
|
||||||
|
@ -5626,6 +5680,8 @@ type Query {
|
||||||
appsInstallations: [AppInstallation!]!
|
appsInstallations: [AppInstallation!]!
|
||||||
apps(filter: AppFilterInput, sortBy: AppSortingInput, before: String, after: String, first: Int, last: Int): AppCountableConnection
|
apps(filter: AppFilterInput, sortBy: AppSortingInput, before: String, after: String, first: Int, last: Int): AppCountableConnection
|
||||||
app(id: ID): App
|
app(id: ID): App
|
||||||
|
appExtensions(filter: AppExtensionFilterInput, before: String, after: String, first: Int, last: Int): AppExtensionCountableConnection
|
||||||
|
appExtension(id: ID!): AppExtension
|
||||||
addressValidationRules(countryCode: CountryCode!, countryArea: String, city: String, cityArea: String): AddressValidationData
|
addressValidationRules(countryCode: CountryCode!, countryArea: String, city: String, cityArea: String): AddressValidationData
|
||||||
address(id: ID!): Address
|
address(id: ID!): Address
|
||||||
customers(filter: CustomerFilterInput, sortBy: UserSortingInput, before: String, after: String, first: Int, last: Int): UserCountableConnection
|
customers(filter: CustomerFilterInput, sortBy: UserSortingInput, before: String, after: String, first: Int, last: Int): UserCountableConnection
|
||||||
|
|
14
src/apps/apolloMocks.ts
Normal file
14
src/apps/apolloMocks.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { MockedResponse } from "@apollo/react-testing";
|
||||||
|
|
||||||
|
import { extensionList } from "./queries";
|
||||||
|
|
||||||
|
export const mocks: MockedResponse[] = [
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
query: extensionList
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
|
@ -11,8 +11,8 @@ import React from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import { App_app } from "../../types/App";
|
import { App_app } from "../../types/App";
|
||||||
|
import { AppFrame } from "../AppFrame";
|
||||||
import { useStyles } from "./styles";
|
import { useStyles } from "./styles";
|
||||||
import useAppConfigLoader from "./useAppConfigLoader";
|
|
||||||
import useSettingsBreadcrumbs from "./useSettingsBreadcrumbs";
|
import useSettingsBreadcrumbs from "./useSettingsBreadcrumbs";
|
||||||
|
|
||||||
export interface AppDetailsSettingsPageProps {
|
export interface AppDetailsSettingsPageProps {
|
||||||
|
@ -34,10 +34,6 @@ export const AppDetailsSettingsPage: React.FC<AppDetailsSettingsPageProps> = ({
|
||||||
const classes = useStyles({});
|
const classes = useStyles({});
|
||||||
const [breadcrumbs, onBreadcrumbClick] = useSettingsBreadcrumbs();
|
const [breadcrumbs, onBreadcrumbClick] = useSettingsBreadcrumbs();
|
||||||
const { sendThemeToExtension } = useTheme();
|
const { sendThemeToExtension } = useTheme();
|
||||||
const frameContainer = useAppConfigLoader(data, backendHost, {
|
|
||||||
onError,
|
|
||||||
onLoad: sendThemeToExtension
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
@ -104,7 +100,17 @@ export const AppDetailsSettingsPage: React.FC<AppDetailsSettingsPageProps> = ({
|
||||||
<Hr />
|
<Hr />
|
||||||
|
|
||||||
<CardSpacer />
|
<CardSpacer />
|
||||||
<div ref={frameContainer} className={classes.iframeContainer} />
|
<div className={classes.iframeContainer}>
|
||||||
|
{data && (
|
||||||
|
<AppFrame
|
||||||
|
src={data.configurationUrl}
|
||||||
|
backendHost={backendHost}
|
||||||
|
appToken={data.accessToken}
|
||||||
|
onLoad={sendThemeToExtension}
|
||||||
|
onError={onError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<CardSpacer />
|
<CardSpacer />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
import { AppFragment } from "@saleor/fragments/types/AppFragment";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import urlJoin from "url-join";
|
|
||||||
|
|
||||||
export type UseAppConfigLoaderCallbacks = Record<
|
|
||||||
"onLoad" | "onError",
|
|
||||||
() => void
|
|
||||||
>;
|
|
||||||
|
|
||||||
function fixRelativeScriptSrc(origin: string) {
|
|
||||||
return (node: HTMLScriptElement) => {
|
|
||||||
// Using node.getAttribute beacuse node.src returns absolute path
|
|
||||||
const src = node.getAttribute("src");
|
|
||||||
if (src?.startsWith("/")) {
|
|
||||||
node.src = urlJoin(origin, src);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchAndSetContent(
|
|
||||||
frameContainer: HTMLDivElement,
|
|
||||||
data: AppFragment,
|
|
||||||
backendHostname: string,
|
|
||||||
{ onError, onLoad }: UseAppConfigLoaderCallbacks
|
|
||||||
) {
|
|
||||||
if (!frameContainer?.innerHTML && data?.configurationUrl) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(data?.configurationUrl, {
|
|
||||||
headers: {
|
|
||||||
"x-saleor-domain": backendHostname,
|
|
||||||
"x-saleor-token": data.accessToken
|
|
||||||
},
|
|
||||||
method: "GET"
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = new URL(response.url);
|
|
||||||
const text = await response.text();
|
|
||||||
const content = new DOMParser().parseFromString(text, "text/html");
|
|
||||||
|
|
||||||
const frame = document.createElement("iframe");
|
|
||||||
frame.src = "about:blank";
|
|
||||||
frame.id = "extension-app";
|
|
||||||
frameContainer.innerHTML = "";
|
|
||||||
frameContainer.appendChild(frame);
|
|
||||||
const frameContent = frame.contentWindow.document;
|
|
||||||
|
|
||||||
const documentElement = content.documentElement;
|
|
||||||
const scriptNodes = documentElement.querySelectorAll("script");
|
|
||||||
|
|
||||||
scriptNodes.forEach(fixRelativeScriptSrc(url.origin));
|
|
||||||
frameContent.write(content.documentElement.innerHTML);
|
|
||||||
frameContent.close();
|
|
||||||
frame.contentWindow.onload = onLoad;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
onError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function useAppConfigLoader(
|
|
||||||
data: AppFragment,
|
|
||||||
backendHost: string,
|
|
||||||
callbacks: UseAppConfigLoaderCallbacks
|
|
||||||
) {
|
|
||||||
const frameContainer = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchAndSetContent(frameContainer.current, data, backendHost, callbacks);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
return frameContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useAppConfigLoader;
|
|
36
src/apps/components/AppDialog/AppDialog.tsx
Normal file
36
src/apps/components/AppDialog/AppDialog.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogProps,
|
||||||
|
DialogTitle,
|
||||||
|
IconButton,
|
||||||
|
Typography
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import CloseIcon from "@material-ui/icons/Close";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useStyles } from "./styles";
|
||||||
|
|
||||||
|
interface AppDialogProps extends DialogProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppDialog: React.FC<AppDialogProps> = ({ children, ...props }) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog aria-labelledby="extension app dialog" {...props}>
|
||||||
|
<DialogTitle disableTypography className={classes.header}>
|
||||||
|
<Typography variant="h6" component="h2">
|
||||||
|
{props.title}
|
||||||
|
</Typography>
|
||||||
|
<IconButton color="inherit" onClick={props.onClose} aria-label="close">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent className={classes.content}>{children}</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppDialog;
|
1
src/apps/components/AppDialog/index.ts
Normal file
1
src/apps/components/AppDialog/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./AppDialog";
|
19
src/apps/components/AppDialog/styles.ts
Normal file
19
src/apps/components/AppDialog/styles.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(
|
||||||
|
() => ({
|
||||||
|
header: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center"
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
width: 600,
|
||||||
|
height: 600
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ name: "AppDialog" }
|
||||||
|
);
|
55
src/apps/components/AppFrame/AppFrame.tsx
Normal file
55
src/apps/components/AppFrame/AppFrame.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { useTheme } from "@saleor/macaw-ui";
|
||||||
|
import React from "react";
|
||||||
|
import urlJoin from "url-join";
|
||||||
|
|
||||||
|
import { useStyles } from "./styles";
|
||||||
|
import { useAppActions } from "./useAppActions";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
src: string;
|
||||||
|
appToken: string;
|
||||||
|
backendHost: string;
|
||||||
|
onLoad?(): void;
|
||||||
|
onError?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOrigin = (url: string) => new URL(url).origin;
|
||||||
|
|
||||||
|
export const AppFrame: React.FC<Props> = ({
|
||||||
|
src,
|
||||||
|
appToken,
|
||||||
|
backendHost,
|
||||||
|
onLoad,
|
||||||
|
onError
|
||||||
|
}) => {
|
||||||
|
const frameRef = React.useRef<HTMLIFrameElement>();
|
||||||
|
const { sendThemeToExtension } = useTheme();
|
||||||
|
const classes = useStyles();
|
||||||
|
const appOrigin = getOrigin(src);
|
||||||
|
const { postToExtension } = useAppActions(frameRef, appOrigin);
|
||||||
|
|
||||||
|
const handleLoad = () => {
|
||||||
|
postToExtension({
|
||||||
|
type: "handshake",
|
||||||
|
payload: {
|
||||||
|
token: appToken,
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sendThemeToExtension();
|
||||||
|
|
||||||
|
if (onLoad) {
|
||||||
|
onLoad();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
ref={frameRef}
|
||||||
|
src={urlJoin(src, `?domain=${backendHost}`)}
|
||||||
|
onError={onError}
|
||||||
|
onLoad={handleLoad}
|
||||||
|
className={classes.iframe}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
1
src/apps/components/AppFrame/index.ts
Normal file
1
src/apps/components/AppFrame/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./AppFrame";
|
12
src/apps/components/AppFrame/styles.ts
Normal file
12
src/apps/components/AppFrame/styles.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(
|
||||||
|
() => ({
|
||||||
|
iframe: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
border: "none"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{ name: "AppFrame" }
|
||||||
|
);
|
74
src/apps/components/AppFrame/useAppActions.ts
Normal file
74
src/apps/components/AppFrame/useAppActions.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { Actions, DispatchResponseEvent, Events } from "@saleor/app-bridge";
|
||||||
|
import useNavigator from "@saleor/hooks/useNavigator";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useExternalApp } from "../ExternalAppContext";
|
||||||
|
|
||||||
|
const sendResponseStatus = (
|
||||||
|
actionId: string,
|
||||||
|
ok: boolean
|
||||||
|
): DispatchResponseEvent => ({
|
||||||
|
type: "response",
|
||||||
|
payload: {
|
||||||
|
actionId,
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useAppActions = (
|
||||||
|
frameEl: React.MutableRefObject<HTMLIFrameElement>,
|
||||||
|
appOrigin: string
|
||||||
|
) => {
|
||||||
|
const navigate = useNavigator();
|
||||||
|
const { closeApp } = useExternalApp();
|
||||||
|
|
||||||
|
const actionReducer = (
|
||||||
|
action: Actions | undefined
|
||||||
|
): DispatchResponseEvent => {
|
||||||
|
switch (action?.type) {
|
||||||
|
case "redirect": {
|
||||||
|
const { to, newContext, actionId } = action.payload;
|
||||||
|
|
||||||
|
if (newContext) {
|
||||||
|
window.open(to);
|
||||||
|
} else if (to.startsWith("/")) {
|
||||||
|
navigate(to);
|
||||||
|
closeApp();
|
||||||
|
} else {
|
||||||
|
window.location.href = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendResponseStatus(actionId, true);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return sendResponseStatus(action?.payload?.actionId, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const postToExtension = (event: Events) => {
|
||||||
|
if (frameEl.current) {
|
||||||
|
frameEl.current.contentWindow.postMessage(event, appOrigin);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handler = (event: MessageEvent<Actions>) => {
|
||||||
|
if (event.origin === appOrigin) {
|
||||||
|
const response = actionReducer(event.data);
|
||||||
|
|
||||||
|
postToExtension(response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", handler);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
postToExtension
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,56 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { AppDialog } from "../AppDialog";
|
||||||
|
import { AppFrame } from "../AppFrame";
|
||||||
|
|
||||||
|
export interface AppData {
|
||||||
|
appToken: string;
|
||||||
|
src: string;
|
||||||
|
backendUrl: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExternalAppContext = React.createContext<{
|
||||||
|
open: boolean;
|
||||||
|
appData: AppData | undefined;
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setAppData: React.Dispatch<React.SetStateAction<AppData | undefined>>;
|
||||||
|
}>(undefined);
|
||||||
|
|
||||||
|
export const ExternalAppProvider: React.FC = ({ children }) => {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const [appData, setAppData] = React.useState<AppData | undefined>();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
setAppData(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExternalAppContext.Provider value={{ open, appData, setOpen, setAppData }}>
|
||||||
|
{children}
|
||||||
|
<AppDialog open={open} onClose={handleClose} title={appData?.label}>
|
||||||
|
{open && appData && (
|
||||||
|
<AppFrame
|
||||||
|
src={appData.src}
|
||||||
|
appToken={appData.appToken}
|
||||||
|
backendHost={appData.backendUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AppDialog>
|
||||||
|
</ExternalAppContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useExternalApp = () => {
|
||||||
|
const { open, setOpen, setAppData } = React.useContext(ExternalAppContext);
|
||||||
|
|
||||||
|
const openApp = (appData: AppData) => {
|
||||||
|
setOpen(true);
|
||||||
|
setAppData(appData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeApp = () => setOpen(false);
|
||||||
|
|
||||||
|
return { open, openApp, closeApp };
|
||||||
|
};
|
1
src/apps/components/ExternalAppContext/index.ts
Normal file
1
src/apps/components/ExternalAppContext/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./ExternalAppContext";
|
|
@ -6,6 +6,7 @@ import gql from "graphql-tag";
|
||||||
import { App, AppVariables } from "./types/App";
|
import { App, AppVariables } from "./types/App";
|
||||||
import { AppsInstallations } from "./types/AppsInstallations";
|
import { AppsInstallations } from "./types/AppsInstallations";
|
||||||
import { AppsList, AppsListVariables } from "./types/AppsList";
|
import { AppsList, AppsListVariables } from "./types/AppsList";
|
||||||
|
import { ExtensionList, ExtensionListVariables } from "./types/ExtensionList";
|
||||||
|
|
||||||
const appsList = gql`
|
const appsList = gql`
|
||||||
query AppsList(
|
query AppsList(
|
||||||
|
@ -72,6 +73,24 @@ const appDetails = gql`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const extensionList = gql`
|
||||||
|
query ExtensionList($filter: AppExtensionFilterInput!) {
|
||||||
|
appExtensions(filter: $filter, first: 20) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
url
|
||||||
|
view
|
||||||
|
type
|
||||||
|
target
|
||||||
|
accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const useAppsListQuery = makeQuery<AppsList, AppsListVariables>(
|
export const useAppsListQuery = makeQuery<AppsList, AppsListVariables>(
|
||||||
appsList
|
appsList
|
||||||
);
|
);
|
||||||
|
@ -81,3 +100,8 @@ export const useAppsInProgressListQuery = makeQuery<AppsInstallations, {}>(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useAppDetails = makeQuery<App, AppVariables>(appDetails);
|
export const useAppDetails = makeQuery<App, AppVariables>(appDetails);
|
||||||
|
|
||||||
|
export const useExtensionList = makeQuery<
|
||||||
|
ExtensionList,
|
||||||
|
ExtensionListVariables
|
||||||
|
>(extensionList);
|
||||||
|
|
39
src/apps/types/ExtensionList.ts
Normal file
39
src/apps/types/ExtensionList.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { AppExtensionFilterInput, AppExtensionViewEnum, AppExtensionTypeEnum, AppExtensionTargetEnum } from "./../../types/globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: ExtensionList
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface ExtensionList_appExtensions_edges_node {
|
||||||
|
__typename: "AppExtension";
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
view: AppExtensionViewEnum;
|
||||||
|
type: AppExtensionTypeEnum;
|
||||||
|
target: AppExtensionTargetEnum;
|
||||||
|
accessToken: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionList_appExtensions_edges {
|
||||||
|
__typename: "AppExtensionCountableEdge";
|
||||||
|
node: ExtensionList_appExtensions_edges_node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionList_appExtensions {
|
||||||
|
__typename: "AppExtensionCountableConnection";
|
||||||
|
edges: ExtensionList_appExtensions_edges[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionList {
|
||||||
|
appExtensions: ExtensionList_appExtensions | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionListVariables {
|
||||||
|
filter: AppExtensionFilterInput;
|
||||||
|
}
|
75
src/apps/useExtensions.ts
Normal file
75
src/apps/useExtensions.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import {
|
||||||
|
AppExtensionTargetEnum,
|
||||||
|
AppExtensionTypeEnum,
|
||||||
|
AppExtensionViewEnum
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
|
import { mapEdgesToItems } from "@saleor/utils/maps";
|
||||||
|
|
||||||
|
import { AppData, useExternalApp } from "./components/ExternalAppContext";
|
||||||
|
import { useExtensionList } from "./queries";
|
||||||
|
import { ExtensionList_appExtensions_edges_node } from "./types/ExtensionList";
|
||||||
|
|
||||||
|
interface Extension {
|
||||||
|
id: string;
|
||||||
|
accessToken: string;
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
open(): void;
|
||||||
|
}
|
||||||
|
type Target = "create" | "moreActions";
|
||||||
|
|
||||||
|
const filterAndMapToTarget = (
|
||||||
|
extensions: ExtensionList_appExtensions_edges_node[],
|
||||||
|
target: AppExtensionTargetEnum,
|
||||||
|
openApp: (appData: AppData) => void
|
||||||
|
): Extension[] =>
|
||||||
|
extensions
|
||||||
|
.filter(app => app.target === target)
|
||||||
|
.map(({ id, accessToken, url, label }) => ({
|
||||||
|
id,
|
||||||
|
accessToken,
|
||||||
|
url,
|
||||||
|
label,
|
||||||
|
open: () =>
|
||||||
|
openApp({ appToken: accessToken, backendUrl: url, src: url, label })
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const mapToMenuItems = (extensions: Extension[]) =>
|
||||||
|
extensions.map(({ label, id, open }) => ({
|
||||||
|
label,
|
||||||
|
testId: `extension-${id}`,
|
||||||
|
onSelect: open
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useExtensions = (
|
||||||
|
view: AppExtensionViewEnum,
|
||||||
|
type: AppExtensionTypeEnum
|
||||||
|
): Record<Target, Extension[]> => {
|
||||||
|
const { openApp } = useExternalApp();
|
||||||
|
const { data } = useExtensionList({
|
||||||
|
fetchPolicy: "cache-first",
|
||||||
|
variables: {
|
||||||
|
filter: {
|
||||||
|
view,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const extensions = mapEdgesToItems(data?.appExtensions) || [];
|
||||||
|
|
||||||
|
const targetCreate = filterAndMapToTarget(
|
||||||
|
extensions,
|
||||||
|
AppExtensionTargetEnum.CREATE,
|
||||||
|
openApp
|
||||||
|
);
|
||||||
|
const targetMoreActions = filterAndMapToTarget(
|
||||||
|
extensions,
|
||||||
|
AppExtensionTargetEnum.MORE_ACTIONS,
|
||||||
|
openApp
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
create: targetCreate,
|
||||||
|
moreActions: targetMoreActions
|
||||||
|
};
|
||||||
|
};
|
116
src/components/ButtonWithSelect/ButtonWithSelect.tsx
Normal file
116
src/components/ButtonWithSelect/ButtonWithSelect.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
ButtonGroupProps,
|
||||||
|
ClickAwayListener,
|
||||||
|
Grow,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
Paper,
|
||||||
|
Popper
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { ArrowDropDown as ArrowDropDownIcon } from "@material-ui/icons";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { useStyles } from "./styles";
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onSelect(e: React.MouseEvent<HTMLLIElement, MouseEvent>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonWithSelectProps
|
||||||
|
extends Omit<ButtonGroupProps, "onClick"> {
|
||||||
|
options: Option[];
|
||||||
|
onClick(e: React.MouseEvent<HTMLButtonElement, MouseEvent>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonWithSelect: React.FC<ButtonWithSelectProps> = ({
|
||||||
|
options,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const anchorRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const handleMenuItemClick = (
|
||||||
|
event: React.MouseEvent<HTMLLIElement, MouseEvent>,
|
||||||
|
onClick: (event: React.MouseEvent<HTMLLIElement, MouseEvent>) => void
|
||||||
|
) => {
|
||||||
|
onClick(event);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
setOpen(prevOpen => !prevOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (event: React.MouseEvent<Document, MouseEvent>) => {
|
||||||
|
if (
|
||||||
|
anchorRef.current &&
|
||||||
|
anchorRef.current.contains(event.target as HTMLElement)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ButtonGroup
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
ref={anchorRef}
|
||||||
|
aria-label="button with select"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Button onClick={onClick}>{children}</Button>
|
||||||
|
{options.length > 0 && (
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
aria-controls={open ? "button-with-select-menu" : undefined}
|
||||||
|
aria-expanded={open ? "true" : undefined}
|
||||||
|
aria-label="select different option"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
onClick={handleToggle}
|
||||||
|
>
|
||||||
|
<ArrowDropDownIcon className={classes.buttonIcon} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ButtonGroup>
|
||||||
|
<Popper
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorRef.current}
|
||||||
|
transition
|
||||||
|
disablePortal
|
||||||
|
placement="bottom-end"
|
||||||
|
className={classes.popper}
|
||||||
|
>
|
||||||
|
{({ TransitionProps }) => (
|
||||||
|
<Grow {...TransitionProps}>
|
||||||
|
<Paper className={classes.paper}>
|
||||||
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
|
<MenuList id="button-with-select-menu">
|
||||||
|
{options.map((option, i) => (
|
||||||
|
<MenuItem
|
||||||
|
key={option.label + i}
|
||||||
|
disabled={option.disabled}
|
||||||
|
onClick={e => handleMenuItemClick(e, option.onSelect)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuList>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Paper>
|
||||||
|
</Grow>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
2
src/components/ButtonWithSelect/index.ts
Normal file
2
src/components/ButtonWithSelect/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./ButtonWithSelect";
|
||||||
|
export { ButtonWithSelect as default } from "./ButtonWithSelect";
|
22
src/components/ButtonWithSelect/styles.ts
Normal file
22
src/components/ButtonWithSelect/styles.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
|
||||||
|
const ITEM_HEIGHT = 48;
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(
|
||||||
|
theme => ({
|
||||||
|
popper: {
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
zIndex: 2
|
||||||
|
},
|
||||||
|
paper: {
|
||||||
|
maxHeight: ITEM_HEIGHT * 4.5,
|
||||||
|
overflowY: "scroll"
|
||||||
|
},
|
||||||
|
buttonIcon: {
|
||||||
|
margin: 0
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "ButtonWithSelect"
|
||||||
|
}
|
||||||
|
);
|
|
@ -40,7 +40,8 @@ const useStyles = makeStyles(
|
||||||
},
|
},
|
||||||
paper: {
|
paper: {
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
maxHeight: ITEM_HEIGHT * 4.5
|
maxHeight: ITEM_HEIGHT * 4.5,
|
||||||
|
overflowY: "scroll"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{ name: "CardMenu" }
|
{ name: "CardMenu" }
|
||||||
|
|
|
@ -11,6 +11,9 @@ const useStyles = makeStyles(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editor: {
|
editor: {
|
||||||
|
"& .codex-editor": {
|
||||||
|
zIndex: 2
|
||||||
|
},
|
||||||
"& .cdx-quote__text": {
|
"& .cdx-quote__text": {
|
||||||
minHeight: 24
|
minHeight: 24
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { handleQueryAuthError } from "@saleor/auth";
|
import { handleQueryAuthError } from "@saleor/auth";
|
||||||
import { RequireAtLeastOne } from "@saleor/misc";
|
import { RequireAtLeastOne } from "@saleor/misc";
|
||||||
import { ApolloQueryResult } from "apollo-client";
|
import { ApolloQueryResult, WatchQueryFetchPolicy } from "apollo-client";
|
||||||
import { DocumentNode } from "graphql";
|
import { DocumentNode } from "graphql";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { QueryResult, useQuery as useBaseQuery } from "react-apollo";
|
import { QueryResult, useQuery as useBaseQuery } from "react-apollo";
|
||||||
|
@ -46,6 +46,7 @@ export type UseQueryOpts<TVariables> = Partial<{
|
||||||
displayLoader: boolean;
|
displayLoader: boolean;
|
||||||
skip: boolean;
|
skip: boolean;
|
||||||
variables: TVariables;
|
variables: TVariables;
|
||||||
|
fetchPolicy: WatchQueryFetchPolicy;
|
||||||
}>;
|
}>;
|
||||||
type UseQueryHook<TData, TVariables> = (
|
type UseQueryHook<TData, TVariables> = (
|
||||||
opts: UseQueryOpts<Omit<TVariables, PrefixedPermissions>>
|
opts: UseQueryOpts<Omit<TVariables, PrefixedPermissions>>
|
||||||
|
@ -57,7 +58,8 @@ function makeQuery<TData, TVariables>(
|
||||||
function useQuery({
|
function useQuery({
|
||||||
displayLoader,
|
displayLoader,
|
||||||
skip,
|
skip,
|
||||||
variables
|
variables,
|
||||||
|
fetchPolicy
|
||||||
}: UseQueryOpts<TVariables>): UseQueryResult<TData, TVariables> {
|
}: UseQueryOpts<TVariables>): UseQueryResult<TData, TVariables> {
|
||||||
const notify = useNotifier();
|
const notify = useNotifier();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
@ -78,7 +80,7 @@ function makeQuery<TData, TVariables>(
|
||||||
useBatching: true
|
useBatching: true
|
||||||
},
|
},
|
||||||
errorPolicy: "all",
|
errorPolicy: "all",
|
||||||
fetchPolicy: "cache-and-network",
|
fetchPolicy: fetchPolicy || "cache-and-network",
|
||||||
onError: error =>
|
onError: error =>
|
||||||
handleQueryAuthError(
|
handleQueryAuthError(
|
||||||
error,
|
error,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { useIntl } from "react-intl";
|
||||||
import { BrowserRouter, Route, Switch } from "react-router-dom";
|
import { BrowserRouter, Route, Switch } from "react-router-dom";
|
||||||
|
|
||||||
import AppsSection from "./apps";
|
import AppsSection from "./apps";
|
||||||
|
import { ExternalAppProvider } from "./apps/components/ExternalAppContext";
|
||||||
import { appsSection } from "./apps/urls";
|
import { appsSection } from "./apps/urls";
|
||||||
import AttributeSection from "./attributes";
|
import AttributeSection from "./attributes";
|
||||||
import { attributeSection } from "./attributes/urls";
|
import { attributeSection } from "./attributes/urls";
|
||||||
|
@ -120,7 +121,9 @@ const App: React.FC = () => (
|
||||||
<ShopProvider>
|
<ShopProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<AppChannelProvider>
|
<AppChannelProvider>
|
||||||
<Routes />
|
<ExternalAppProvider>
|
||||||
|
<Routes />
|
||||||
|
</ExternalAppProvider>
|
||||||
</AppChannelProvider>
|
</AppChannelProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ShopProvider>
|
</ShopProvider>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { Button, Card } from "@material-ui/core";
|
import { Card } from "@material-ui/core";
|
||||||
|
import { mapToMenuItems, useExtensions } from "@saleor/apps/useExtensions";
|
||||||
|
import { ButtonWithSelect } from "@saleor/components/ButtonWithSelect";
|
||||||
import CardMenu from "@saleor/components/CardMenu";
|
import CardMenu from "@saleor/components/CardMenu";
|
||||||
import ColumnPicker, {
|
import ColumnPicker, {
|
||||||
ColumnPickerChoice
|
ColumnPickerChoice
|
||||||
|
@ -22,6 +24,10 @@ import {
|
||||||
PageListProps,
|
PageListProps,
|
||||||
SortPage
|
SortPage
|
||||||
} from "@saleor/types";
|
} from "@saleor/types";
|
||||||
|
import {
|
||||||
|
AppExtensionTypeEnum,
|
||||||
|
AppExtensionViewEnum
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
import { hasLimits, isLimitReached } from "@saleor/utils/limits";
|
import { hasLimits, isLimitReached } from "@saleor/utils/limits";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
@ -129,6 +135,13 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
];
|
];
|
||||||
|
|
||||||
const limitReached = isLimitReached(limits, "productVariants");
|
const limitReached = isLimitReached(limits, "productVariants");
|
||||||
|
const { create, moreActions } = useExtensions(
|
||||||
|
AppExtensionViewEnum.PRODUCT,
|
||||||
|
AppExtensionTypeEnum.OVERVIEW
|
||||||
|
);
|
||||||
|
|
||||||
|
const extensionMenuItems = mapToMenuItems(moreActions);
|
||||||
|
const extensionCreateButtonItems = mapToMenuItems(create);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
@ -158,7 +171,8 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
}),
|
}),
|
||||||
onSelect: onExport,
|
onSelect: onExport,
|
||||||
testId: "export"
|
testId: "export"
|
||||||
}
|
},
|
||||||
|
...extensionMenuItems
|
||||||
]}
|
]}
|
||||||
data-test="menu"
|
data-test="menu"
|
||||||
/>
|
/>
|
||||||
|
@ -176,18 +190,17 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
||||||
onFetchMore={onFetchMore}
|
onFetchMore={onFetchMore}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
/>
|
/>
|
||||||
<Button
|
<ButtonWithSelect
|
||||||
|
options={extensionCreateButtonItems}
|
||||||
|
data-test="add-product"
|
||||||
disabled={limitReached}
|
disabled={limitReached}
|
||||||
onClick={onAdd}
|
onClick={onAdd}
|
||||||
color="primary"
|
|
||||||
variant="contained"
|
|
||||||
data-test="add-product"
|
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Create Product"
|
defaultMessage="Create Product"
|
||||||
description="button"
|
description="button"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</ButtonWithSelect>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
{limitReached && (
|
{limitReached && (
|
||||||
<LimitReachedAlert
|
<LimitReachedAlert
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { OutputData } from "@editorjs/editorjs";
|
import { OutputData } from "@editorjs/editorjs";
|
||||||
|
import { mapToMenuItems, useExtensions } from "@saleor/apps/useExtensions";
|
||||||
import {
|
import {
|
||||||
getAttributeValuesFromReferences,
|
getAttributeValuesFromReferences,
|
||||||
mergeAttributeValues
|
mergeAttributeValues
|
||||||
|
@ -6,6 +7,7 @@ import {
|
||||||
import { ChannelData } from "@saleor/channels/utils";
|
import { ChannelData } from "@saleor/channels/utils";
|
||||||
import AssignAttributeValueDialog from "@saleor/components/AssignAttributeValueDialog";
|
import AssignAttributeValueDialog from "@saleor/components/AssignAttributeValueDialog";
|
||||||
import Attributes, { AttributeInput } from "@saleor/components/Attributes";
|
import Attributes, { AttributeInput } from "@saleor/components/Attributes";
|
||||||
|
import CardMenu from "@saleor/components/CardMenu";
|
||||||
import CardSpacer from "@saleor/components/CardSpacer";
|
import CardSpacer from "@saleor/components/CardSpacer";
|
||||||
import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard";
|
import ChannelsAvailabilityCard from "@saleor/components/ChannelsAvailabilityCard";
|
||||||
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
|
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
|
||||||
|
@ -40,7 +42,11 @@ import {
|
||||||
ListActions,
|
ListActions,
|
||||||
ReorderAction
|
ReorderAction
|
||||||
} from "@saleor/types";
|
} from "@saleor/types";
|
||||||
import { PermissionEnum } from "@saleor/types/globalTypes";
|
import {
|
||||||
|
AppExtensionTypeEnum,
|
||||||
|
AppExtensionViewEnum,
|
||||||
|
PermissionEnum
|
||||||
|
} from "@saleor/types/globalTypes";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
@ -243,6 +249,13 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
||||||
onCloseDialog();
|
onCloseDialog();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { moreActions } = useExtensions(
|
||||||
|
AppExtensionViewEnum.PRODUCT,
|
||||||
|
AppExtensionTypeEnum.DETAILS
|
||||||
|
);
|
||||||
|
|
||||||
|
const extensionMenuItems = mapToMenuItems(moreActions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProductUpdateForm
|
<ProductUpdateForm
|
||||||
isSimpleProduct={isSimpleProduct}
|
isSimpleProduct={isSimpleProduct}
|
||||||
|
@ -283,7 +296,11 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
||||||
<Backlink onClick={onBack}>
|
<Backlink onClick={onBack}>
|
||||||
{intl.formatMessage(sectionNames.products)}
|
{intl.formatMessage(sectionNames.products)}
|
||||||
</Backlink>
|
</Backlink>
|
||||||
<PageHeader title={header} />
|
<PageHeader title={header}>
|
||||||
|
{extensionMenuItems.length > 0 && (
|
||||||
|
<CardMenu menuItems={extensionMenuItems} data-test="menu" />
|
||||||
|
)}
|
||||||
|
</PageHeader>
|
||||||
<Grid>
|
<Grid>
|
||||||
<div>
|
<div>
|
||||||
<ProductDetailsForm
|
<ProductDetailsForm
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ExternalAppProvider } from "@saleor/apps/components/ExternalAppContext";
|
||||||
import { Locale, RawLocaleProvider } from "@saleor/components/Locale";
|
import { Locale, RawLocaleProvider } from "@saleor/components/Locale";
|
||||||
import { ThemeProvider } from "@saleor/macaw-ui";
|
import { ThemeProvider } from "@saleor/macaw-ui";
|
||||||
import themeOverrides from "@saleor/themeOverrides";
|
import themeOverrides from "@saleor/themeOverrides";
|
||||||
|
@ -5,37 +6,42 @@ import React from "react";
|
||||||
import { IntlProvider } from "react-intl";
|
import { IntlProvider } from "react-intl";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ApolloMockedProvider } from "../../testUtils/ApolloMockedProvider";
|
||||||
import { Provider as DateProvider } from "../components/Date/DateContext";
|
import { Provider as DateProvider } from "../components/Date/DateContext";
|
||||||
import MessageManagerProvider from "../components/messages";
|
import MessageManagerProvider from "../components/messages";
|
||||||
import { TimezoneProvider } from "../components/Timezone";
|
import { TimezoneProvider } from "../components/Timezone";
|
||||||
import { APP_MOUNT_URI } from "../config";
|
import { APP_MOUNT_URI } from "../config";
|
||||||
|
|
||||||
export const Decorator = storyFn => (
|
export const Decorator = storyFn => (
|
||||||
<IntlProvider defaultLocale={Locale.EN} locale={Locale.EN}>
|
<ApolloMockedProvider>
|
||||||
<RawLocaleProvider
|
<IntlProvider defaultLocale={Locale.EN} locale={Locale.EN}>
|
||||||
value={{
|
<RawLocaleProvider
|
||||||
locale: Locale.EN,
|
value={{
|
||||||
setLocale: () => undefined
|
locale: Locale.EN,
|
||||||
}}
|
setLocale: () => undefined
|
||||||
>
|
}}
|
||||||
<DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
|
>
|
||||||
<TimezoneProvider value="America/New_York">
|
<DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
|
||||||
<ThemeProvider overrides={themeOverrides}>
|
<TimezoneProvider value="America/New_York">
|
||||||
<BrowserRouter basename={APP_MOUNT_URI}>
|
<ThemeProvider overrides={themeOverrides}>
|
||||||
<MessageManagerProvider>
|
<BrowserRouter basename={APP_MOUNT_URI}>
|
||||||
<div
|
<ExternalAppProvider>
|
||||||
style={{
|
<MessageManagerProvider>
|
||||||
padding: 24
|
<div
|
||||||
}}
|
style={{
|
||||||
>
|
padding: 24
|
||||||
{storyFn()}
|
}}
|
||||||
</div>
|
>
|
||||||
</MessageManagerProvider>
|
{storyFn()}
|
||||||
</BrowserRouter>
|
</div>
|
||||||
</ThemeProvider>
|
</MessageManagerProvider>
|
||||||
</TimezoneProvider>
|
</ExternalAppProvider>
|
||||||
</DateProvider>
|
</BrowserRouter>
|
||||||
</RawLocaleProvider>
|
</ThemeProvider>
|
||||||
</IntlProvider>
|
</TimezoneProvider>
|
||||||
|
</DateProvider>
|
||||||
|
</RawLocaleProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
</ApolloMockedProvider>
|
||||||
);
|
);
|
||||||
export default Decorator;
|
export default Decorator;
|
||||||
|
|
|
@ -23485,7 +23485,12 @@ exports[`Storyshots Views / Apps / App details settings default 1`] = `
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="AppDetailsSettingsPage-iframeContainer-id"
|
class="AppDetailsSettingsPage-iframeContainer-id"
|
||||||
/>
|
>
|
||||||
|
<iframe
|
||||||
|
class="AppFrame-iframe-id"
|
||||||
|
src="htpp://localhost:8888/configuration?domain=host"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="CardSpacer-spacer-id"
|
class="CardSpacer-spacer-id"
|
||||||
/>
|
/>
|
||||||
|
@ -213135,18 +213140,24 @@ exports[`Storyshots Views / Products / Product list default 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div
|
||||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id"
|
aria-label="button with select"
|
||||||
|
class="MuiButtonGroup-root-id MuiButtonGroup-contained-id"
|
||||||
data-test="add-product"
|
data-test="add-product"
|
||||||
tabindex="0"
|
role="group"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<span
|
<button
|
||||||
class="MuiButton-label-id"
|
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedContained-id MuiButtonGroup-groupedContainedHorizontal-id MuiButtonGroup-groupedContainedPrimary-id MuiButton-containedPrimary-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Create Product
|
<span
|
||||||
</span>
|
class="MuiButton-label-id"
|
||||||
</button>
|
>
|
||||||
|
Create Product
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -215638,19 +215649,25 @@ exports[`Storyshots Views / Products / Product list limits reached 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div
|
||||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id MuiButton-disabled-id MuiButtonBase-disabled-id"
|
aria-label="button with select"
|
||||||
|
class="MuiButtonGroup-root-id MuiButtonGroup-contained-id"
|
||||||
data-test="add-product"
|
data-test="add-product"
|
||||||
disabled=""
|
role="group"
|
||||||
tabindex="-1"
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<span
|
<button
|
||||||
class="MuiButton-label-id"
|
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedContained-id MuiButtonGroup-groupedContainedHorizontal-id MuiButtonGroup-groupedContainedPrimary-id MuiButtonGroup-disabled-id MuiButton-containedPrimary-id MuiButton-disabled-id MuiButtonBase-disabled-id"
|
||||||
|
disabled=""
|
||||||
|
tabindex="-1"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Create Product
|
<span
|
||||||
</span>
|
class="MuiButton-label-id"
|
||||||
</button>
|
>
|
||||||
|
Create Product
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -218191,18 +218208,24 @@ exports[`Storyshots Views / Products / Product list loading 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div
|
||||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id"
|
aria-label="button with select"
|
||||||
|
class="MuiButtonGroup-root-id MuiButtonGroup-contained-id"
|
||||||
data-test="add-product"
|
data-test="add-product"
|
||||||
tabindex="0"
|
role="group"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<span
|
<button
|
||||||
class="MuiButton-label-id"
|
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedContained-id MuiButtonGroup-groupedContainedHorizontal-id MuiButtonGroup-groupedContainedPrimary-id MuiButton-containedPrimary-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Create Product
|
<span
|
||||||
</span>
|
class="MuiButton-label-id"
|
||||||
</button>
|
>
|
||||||
|
Create Product
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -218750,18 +218773,24 @@ exports[`Storyshots Views / Products / Product list no channels 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div
|
||||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id"
|
aria-label="button with select"
|
||||||
|
class="MuiButtonGroup-root-id MuiButtonGroup-contained-id"
|
||||||
data-test="add-product"
|
data-test="add-product"
|
||||||
tabindex="0"
|
role="group"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<span
|
<button
|
||||||
class="MuiButton-label-id"
|
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedContained-id MuiButtonGroup-groupedContainedHorizontal-id MuiButtonGroup-groupedContainedPrimary-id MuiButton-containedPrimary-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Create Product
|
<span
|
||||||
</span>
|
class="MuiButton-label-id"
|
||||||
</button>
|
>
|
||||||
|
Create Product
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -220953,18 +220982,24 @@ exports[`Storyshots Views / Products / Product list no data 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div
|
||||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id"
|
aria-label="button with select"
|
||||||
|
class="MuiButtonGroup-root-id MuiButtonGroup-contained-id"
|
||||||
data-test="add-product"
|
data-test="add-product"
|
||||||
tabindex="0"
|
role="group"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<span
|
<button
|
||||||
class="MuiButton-label-id"
|
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedContained-id MuiButtonGroup-groupedContainedHorizontal-id MuiButtonGroup-groupedContainedPrimary-id MuiButton-containedPrimary-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Create Product
|
<span
|
||||||
</span>
|
class="MuiButton-label-id"
|
||||||
</button>
|
>
|
||||||
|
Create Product
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -221372,18 +221407,24 @@ exports[`Storyshots Views / Products / Product list no limits 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div
|
||||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id"
|
aria-label="button with select"
|
||||||
|
class="MuiButtonGroup-root-id MuiButtonGroup-contained-id"
|
||||||
data-test="add-product"
|
data-test="add-product"
|
||||||
tabindex="0"
|
role="group"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<span
|
<button
|
||||||
class="MuiButton-label-id"
|
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedContained-id MuiButtonGroup-groupedContainedHorizontal-id MuiButtonGroup-groupedContainedPrimary-id MuiButton-containedPrimary-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Create Product
|
<span
|
||||||
</span>
|
class="MuiButton-label-id"
|
||||||
</button>
|
>
|
||||||
|
Create Product
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -223875,18 +223916,24 @@ exports[`Storyshots Views / Products / Product list with data 1`] = `
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div
|
||||||
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButton-containedPrimary-id"
|
aria-label="button with select"
|
||||||
|
class="MuiButtonGroup-root-id MuiButtonGroup-contained-id"
|
||||||
data-test="add-product"
|
data-test="add-product"
|
||||||
tabindex="0"
|
role="group"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<span
|
<button
|
||||||
class="MuiButton-label-id"
|
class="MuiButtonBase-root-id MuiButton-root-id MuiButton-contained-id MuiButtonGroup-grouped-id MuiButtonGroup-groupedHorizontal-id MuiButtonGroup-groupedContained-id MuiButtonGroup-groupedContainedHorizontal-id MuiButtonGroup-groupedContainedPrimary-id MuiButton-containedPrimary-id"
|
||||||
|
tabindex="0"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
Create Product
|
<span
|
||||||
</span>
|
class="MuiButton-label-id"
|
||||||
</button>
|
>
|
||||||
|
Create Product
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -63,6 +63,20 @@ export enum AppErrorCode {
|
||||||
UNIQUE = "UNIQUE",
|
UNIQUE = "UNIQUE",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AppExtensionTargetEnum {
|
||||||
|
CREATE = "CREATE",
|
||||||
|
MORE_ACTIONS = "MORE_ACTIONS",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AppExtensionTypeEnum {
|
||||||
|
DETAILS = "DETAILS",
|
||||||
|
OVERVIEW = "OVERVIEW",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AppExtensionViewEnum {
|
||||||
|
PRODUCT = "PRODUCT",
|
||||||
|
}
|
||||||
|
|
||||||
export enum AppSortField {
|
export enum AppSortField {
|
||||||
CREATION_DATE = "CREATION_DATE",
|
CREATION_DATE = "CREATION_DATE",
|
||||||
NAME = "NAME",
|
NAME = "NAME",
|
||||||
|
@ -1833,6 +1847,12 @@ export interface AddressInput {
|
||||||
phone?: string | null;
|
phone?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppExtensionFilterInput {
|
||||||
|
view?: AppExtensionViewEnum | null;
|
||||||
|
type?: AppExtensionTypeEnum | null;
|
||||||
|
target?: AppExtensionTargetEnum | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppFilterInput {
|
export interface AppFilterInput {
|
||||||
search?: string | null;
|
search?: string | null;
|
||||||
isActive?: boolean | null;
|
isActive?: boolean | null;
|
||||||
|
|
10
testUtils/ApolloMockedProvider.tsx
Normal file
10
testUtils/ApolloMockedProvider.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { MockedProvider, MockedResponse } from "@apollo/react-testing";
|
||||||
|
// mocks
|
||||||
|
import { mocks as appMocks } from "@saleor/apps/apolloMocks";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const mocks: MockedResponse[] = [...appMocks];
|
||||||
|
|
||||||
|
export const ApolloMockedProvider = ({ children }) => (
|
||||||
|
<MockedProvider mocks={mocks}>{children}</MockedProvider>
|
||||||
|
);
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ExternalAppProvider } from "@saleor/apps/components/ExternalAppContext";
|
||||||
import { Provider as DateProvider } from "@saleor/components/Date/DateContext";
|
import { Provider as DateProvider } from "@saleor/components/Date/DateContext";
|
||||||
import { Locale, RawLocaleProvider } from "@saleor/components/Locale";
|
import { Locale, RawLocaleProvider } from "@saleor/components/Locale";
|
||||||
import { TimezoneProvider } from "@saleor/components/Timezone";
|
import { TimezoneProvider } from "@saleor/components/Timezone";
|
||||||
|
@ -5,21 +6,27 @@ import { ThemeProvider } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IntlProvider } from "react-intl";
|
import { IntlProvider } from "react-intl";
|
||||||
|
|
||||||
|
import { ApolloMockedProvider } from "./ApolloMockedProvider";
|
||||||
|
|
||||||
const Wrapper: React.FC = ({ children }) => (
|
const Wrapper: React.FC = ({ children }) => (
|
||||||
<IntlProvider defaultLocale={Locale.EN} locale={Locale.EN}>
|
<ApolloMockedProvider>
|
||||||
<RawLocaleProvider
|
<IntlProvider defaultLocale={Locale.EN} locale={Locale.EN}>
|
||||||
value={{
|
<RawLocaleProvider
|
||||||
locale: Locale.EN,
|
value={{
|
||||||
setLocale: () => undefined
|
locale: Locale.EN,
|
||||||
}}
|
setLocale: () => undefined
|
||||||
>
|
}}
|
||||||
<DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
|
>
|
||||||
<TimezoneProvider value="America/New_York">
|
<DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
|
||||||
<ThemeProvider>{children}</ThemeProvider>
|
<TimezoneProvider value="America/New_York">
|
||||||
</TimezoneProvider>
|
<ThemeProvider>
|
||||||
</DateProvider>
|
<ExternalAppProvider>{children}</ExternalAppProvider>
|
||||||
</RawLocaleProvider>
|
</ThemeProvider>
|
||||||
</IntlProvider>
|
</TimezoneProvider>
|
||||||
|
</DateProvider>
|
||||||
|
</RawLocaleProvider>
|
||||||
|
</IntlProvider>
|
||||||
|
</ApolloMockedProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Wrapper;
|
export default Wrapper;
|
||||||
|
|
Loading…
Reference in a new issue