+
= props => {
className={classes.iconButton}
color="primary"
disabled={disabled}
- onClick={handleClick}
+ ref={anchorRef}
+ onClick={handleToggle}
>
-
+
+
+
+
+
+
+ )}
+
);
};
diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx
index 53a56bcdf..5198817da 100644
--- a/src/components/Chip/Chip.tsx
+++ b/src/components/Chip/Chip.tsx
@@ -23,8 +23,8 @@ const useStyles = makeStyles(
color: theme.palette.common.white
},
root: {
- background: fade(theme.palette.secondary.main, 0.8),
- borderRadius: 8,
+ background: fade(theme.palette.primary.main, 0.8),
+ borderRadius: 18,
display: "inline-block",
marginRight: theme.spacing(2),
padding: "6px 12px"
diff --git a/src/components/CreatorSteps/CreatorSteps.tsx b/src/components/CreatorSteps/CreatorSteps.tsx
new file mode 100644
index 000000000..d7be7c4a3
--- /dev/null
+++ b/src/components/CreatorSteps/CreatorSteps.tsx
@@ -0,0 +1,84 @@
+import { makeStyles } from "@material-ui/core/styles";
+import Typography from "@material-ui/core/Typography";
+import classNames from "classnames";
+import React from "react";
+
+export interface Step
{
+ label: string;
+ value: T;
+}
+
+const useStyles = makeStyles(
+ theme => ({
+ label: {
+ fontSize: 14,
+ textAlign: "center"
+ },
+ root: {
+ borderBottom: `1px solid ${theme.palette.divider}`,
+ display: "flex",
+ justifyContent: "space-between",
+ marginBottom: theme.spacing(3)
+ },
+ tab: {
+ flex: 1,
+ paddingBottom: theme.spacing(),
+ userSelect: "none"
+ },
+ tabActive: {
+ fontWeight: 600
+ },
+ tabVisited: {
+ borderBottom: `3px solid ${theme.palette.primary.main}`,
+ cursor: "pointer"
+ }
+ }),
+ {
+ name: "CreatorSteps"
+ }
+);
+
+export interface CreatorStepsProps {
+ currentStep: T;
+ steps: Array>;
+ onStepClick: (step: T) => void;
+}
+
+function makeCreatorSteps() {
+ const CreatorSteps: React.FC> = ({
+ currentStep,
+ steps,
+ onStepClick
+ }) => {
+ const classes = useStyles({});
+
+ return (
+
+ {steps.map((step, stepIndex) => {
+ const visitedStep =
+ steps.findIndex(step => step.value === currentStep) >= stepIndex;
+
+ return (
+
onStepClick(step.value) : undefined}
+ key={step.value}
+ >
+
+ {step.label}
+
+
+ );
+ })}
+
+ );
+ };
+ CreatorSteps.displayName = "CreatorSteps";
+
+ return CreatorSteps;
+}
+
+export default makeCreatorSteps;
diff --git a/src/components/CreatorSteps/index.ts b/src/components/CreatorSteps/index.ts
new file mode 100644
index 000000000..5ab26400a
--- /dev/null
+++ b/src/components/CreatorSteps/index.ts
@@ -0,0 +1,2 @@
+export * from "./CreatorSteps";
+export { default } from "./CreatorSteps";
diff --git a/src/components/RadioGroupField/RadioGroupField.tsx b/src/components/RadioGroupField/RadioGroupField.tsx
index e44c9352e..2ef01e5a7 100644
--- a/src/components/RadioGroupField/RadioGroupField.tsx
+++ b/src/components/RadioGroupField/RadioGroupField.tsx
@@ -36,8 +36,11 @@ const useStyles = makeStyles(
}
);
-export interface RadioGroupFieldChoice {
- value: string;
+export interface RadioGroupFieldChoice<
+ T extends string | number = string | number
+> {
+ disabled?: boolean;
+ value: T;
label: React.ReactNode;
}
@@ -49,7 +52,7 @@ interface RadioGroupFieldProps {
hint?: string;
label?: string;
name?: string;
- value: string;
+ value: string | number;
onChange: (event: React.ChangeEvent) => void;
}
@@ -87,6 +90,7 @@ export const RadioGroupField: React.FC = props => {
{choices.length > 0 ? (
choices.map(choice => (
}
diff --git a/src/components/messages/index.ts b/src/components/messages/index.ts
index 264a4fa35..f2e574ae3 100644
--- a/src/components/messages/index.ts
+++ b/src/components/messages/index.ts
@@ -9,7 +9,7 @@ export interface IMessage {
autohide?: number;
expandText?: string;
title?: string;
- text: string;
+ text: React.ReactNode;
onUndo?: () => void;
status?: Status;
}
diff --git a/src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx b/src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx
index 0151816e0..8c5e46fa3 100644
--- a/src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx
+++ b/src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx
@@ -1,16 +1,30 @@
+import { JobStatusEnum } from "@saleor/types/globalTypes";
import { renderHook } from "@testing-library/react-hooks";
-import { createMockClient } from "mock-apollo-client";
+import { createMockClient, RequestHandlerResponse } from "mock-apollo-client";
import {
backgroundTasksRefreshTime,
useBackgroundTasks
} from "./BackgroundTasksProvider";
-import { OnCompletedTaskData, Task, TaskData, TaskStatus } from "./types";
+import { checkExportFileStatus } from "./queries";
+import { Task, TaskData, TaskStatus } from "./types";
+import { CheckExportFileStatus } from "./types/CheckExportFileStatus";
jest.useFakeTimers();
function renderBackgroundTasks() {
const mockClient = createMockClient();
+ mockClient.setRequestHandler(checkExportFileStatus, () =>
+ Promise.resolve>({
+ data: {
+ exportFile: {
+ __typename: "ExportFile",
+ id: "123",
+ status: JobStatusEnum.SUCCESS
+ }
+ }
+ })
+ );
const intl = {
formatMessage: ({ defaultMessage }) => defaultMessage
};
@@ -80,33 +94,6 @@ describe("Background task provider", () => {
});
});
- it("can handle task failure", done => {
- const handle = jest.fn, []>(
- () => new Promise(resolve => resolve(TaskStatus.FAILURE))
- );
- const onCompleted = jest.fn((data: OnCompletedTaskData) =>
- expect(data.status).toBe(TaskStatus.FAILURE)
- );
- const onError = jest.fn();
-
- const { result } = renderBackgroundTasks();
-
- result.current.queue(Task.CUSTOM, {
- handle,
- onCompleted,
- onError
- });
-
- jest.runOnlyPendingTimers();
-
- setImmediate(() => {
- expect(handle).toHaveBeenCalledTimes(1);
- expect(onCompleted).toHaveBeenCalledTimes(1);
-
- done();
- });
- });
-
it("can cancel task", done => {
const onCompleted = jest.fn();
diff --git a/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx b/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx
index 600addac1..b9ec40fe2 100644
--- a/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx
+++ b/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx
@@ -1,13 +1,18 @@
import { IMessageContext } from "@saleor/components/messages";
import useNotifier from "@saleor/hooks/useNotifier";
-import { checkOrderInvoicesStatus } from "@saleor/orders/queries";
import ApolloClient from "apollo-client";
import React from "react";
import { useApolloClient } from "react-apollo";
import { IntlShape, useIntl } from "react-intl";
import BackgroundTasksContext from "./context";
-import { handleTask, queueCustom, queueInvoiceGenerate } from "./tasks";
+import { checkExportFileStatus, checkOrderInvoicesStatus } from "./queries";
+import {
+ handleTask,
+ queueCustom,
+ queueExport,
+ queueInvoiceGenerate
+} from "./tasks";
import { QueuedTask, Task, TaskData, TaskStatus } from "./types";
export const backgroundTasksRefreshTime = 15 * 1000;
@@ -81,6 +86,22 @@ export function useBackgroundTasks(
intl
);
break;
+ case Task.EXPORT:
+ queueExport(
+ idCounter.current,
+ tasks,
+ () =>
+ apolloClient.query({
+ fetchPolicy: "network-only",
+ query: checkExportFileStatus,
+ variables: {
+ id: data.id
+ }
+ }),
+ notify,
+ intl
+ );
+ break;
}
return idCounter.current;
diff --git a/src/containers/BackgroundTasks/messages.ts b/src/containers/BackgroundTasks/messages.ts
new file mode 100644
index 000000000..6b7ab069c
--- /dev/null
+++ b/src/containers/BackgroundTasks/messages.ts
@@ -0,0 +1,27 @@
+import { defineMessages } from "react-intl";
+
+export default defineMessages({
+ exportFailedTitle: {
+ defaultMessage: "Product Export Failed"
+ },
+ exportFinishedText: {
+ defaultMessage:
+ "Product export has finished and was sent to your email address."
+ },
+ exportFinishedTitle: {
+ defaultMessage: "Exporting CSV finished",
+ description: "csv file exporting has finished, header"
+ },
+ invoiceGenerateFinishedText: {
+ defaultMessage:
+ "Requested Invoice was generated. It was added to the top of the invoice list on this view. Enjoy!"
+ },
+ invoiceGenerateFinishedTitle: {
+ defaultMessage: "Invoice Generated",
+ description: "invoice generating has finished, header"
+ },
+ invoiceGenerationFailedTitle: {
+ defaultMessage: "Invoice Generation",
+ description: "dialog header, title"
+ }
+});
diff --git a/src/containers/BackgroundTasks/queries.ts b/src/containers/BackgroundTasks/queries.ts
new file mode 100644
index 000000000..c34f04d04
--- /dev/null
+++ b/src/containers/BackgroundTasks/queries.ts
@@ -0,0 +1,23 @@
+import { invoiceFragment } from "@saleor/fragments/orders";
+import gql from "graphql-tag";
+
+export const checkExportFileStatus = gql`
+ query CheckExportFileStatus($id: ID!) {
+ exportFile(id: $id) {
+ id
+ status
+ }
+ }
+`;
+
+export const checkOrderInvoicesStatus = gql`
+ ${invoiceFragment}
+ query CheckOrderInvoicesStatus($id: ID!) {
+ order(id: $id) {
+ id
+ invoices {
+ ...InvoiceFragment
+ }
+ }
+ }
+`;
diff --git a/src/containers/BackgroundTasks/tasks.ts b/src/containers/BackgroundTasks/tasks.ts
index a1adcdf27..fb506ffd7 100644
--- a/src/containers/BackgroundTasks/tasks.ts
+++ b/src/containers/BackgroundTasks/tasks.ts
@@ -1,31 +1,29 @@
import { IMessageContext } from "@saleor/components/messages";
import { commonMessages } from "@saleor/intl";
-import { CheckOrderInvoicesStatus } from "@saleor/orders/types/CheckOrderInvoicesStatus";
import { JobStatusEnum } from "@saleor/types/globalTypes";
import { ApolloQueryResult } from "apollo-client";
-import { defineMessages, IntlShape } from "react-intl";
+import { IntlShape } from "react-intl";
+import messages from "./messages";
import {
InvoiceGenerateParams,
QueuedTask,
TaskData,
TaskStatus
} from "./types";
+import { CheckExportFileStatus } from "./types/CheckExportFileStatus";
+import { CheckOrderInvoicesStatus } from "./types/CheckOrderInvoicesStatus";
-export const messages = defineMessages({
- invoiceGenerateFinishedText: {
- defaultMessage:
- "Requested Invoice was generated. It was added to the top of the invoice list on this view. Enjoy!"
- },
- invoiceGenerateFinishedTitle: {
- defaultMessage: "Invoice Generated",
- description: "invoice generating has finished, header"
- },
- invoiceGenerationFailedTitle: {
- defaultMessage: "Invoice Generation",
- description: "dialog header, title"
+function getTaskStatus(jobStatus: JobStatusEnum): TaskStatus {
+ switch (jobStatus) {
+ case JobStatusEnum.SUCCESS:
+ return TaskStatus.SUCCESS;
+ case JobStatusEnum.PENDING:
+ return TaskStatus.PENDING;
+ default:
+ return TaskStatus.FAILURE;
}
-});
+}
export async function handleTask(task: QueuedTask): Promise {
let status = TaskStatus.PENDING;
@@ -89,11 +87,7 @@ export function queueInvoiceGenerate(
invoice => invoice.id === generateInvoice.invoiceId
).status;
- return status === JobStatusEnum.SUCCESS
- ? TaskStatus.SUCCESS
- : status === JobStatusEnum.PENDING
- ? TaskStatus.PENDING
- : TaskStatus.FAILURE;
+ return getTaskStatus(status);
},
id,
onCompleted: data =>
@@ -104,6 +98,7 @@ export function queueInvoiceGenerate(
title: intl.formatMessage(messages.invoiceGenerateFinishedTitle)
})
: notify({
+ status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong),
title: intl.formatMessage(messages.invoiceGenerationFailedTitle)
}),
@@ -112,3 +107,38 @@ export function queueInvoiceGenerate(
}
];
}
+
+export function queueExport(
+ id: number,
+ tasks: React.MutableRefObject,
+ fetch: () => Promise>,
+ notify: IMessageContext,
+ intl: IntlShape
+) {
+ tasks.current = [
+ ...tasks.current,
+ {
+ handle: async () => {
+ const result = await fetch();
+ const status = result.data.exportFile.status;
+
+ return getTaskStatus(status);
+ },
+ id,
+ onCompleted: data =>
+ data.status === TaskStatus.SUCCESS
+ ? notify({
+ status: "success",
+ text: intl.formatMessage(messages.exportFinishedText),
+ title: intl.formatMessage(messages.exportFinishedTitle)
+ })
+ : notify({
+ status: "error",
+ text: intl.formatMessage(commonMessages.somethingWentWrong),
+ title: intl.formatMessage(messages.exportFailedTitle)
+ }),
+ onError: handleError,
+ status: TaskStatus.PENDING
+ }
+ ];
+}
diff --git a/src/containers/BackgroundTasks/types.ts b/src/containers/BackgroundTasks/types.ts
index b927a0554..269fd3791 100644
--- a/src/containers/BackgroundTasks/types.ts
+++ b/src/containers/BackgroundTasks/types.ts
@@ -1,5 +1,6 @@
export enum Task {
CUSTOM,
+ EXPORT,
INVOICE_GENERATE
}
export enum TaskStatus {
diff --git a/src/containers/BackgroundTasks/types/CheckExportFileStatus.ts b/src/containers/BackgroundTasks/types/CheckExportFileStatus.ts
new file mode 100644
index 000000000..210e7e474
--- /dev/null
+++ b/src/containers/BackgroundTasks/types/CheckExportFileStatus.ts
@@ -0,0 +1,23 @@
+/* tslint:disable */
+/* eslint-disable */
+// This file was automatically generated and should not be edited.
+
+import { JobStatusEnum } from "./../../../types/globalTypes";
+
+// ====================================================
+// GraphQL query operation: CheckExportFileStatus
+// ====================================================
+
+export interface CheckExportFileStatus_exportFile {
+ __typename: "ExportFile";
+ id: string;
+ status: JobStatusEnum;
+}
+
+export interface CheckExportFileStatus {
+ exportFile: CheckExportFileStatus_exportFile | null;
+}
+
+export interface CheckExportFileStatusVariables {
+ id: string;
+}
diff --git a/src/orders/types/CheckOrderInvoicesStatus.ts b/src/containers/BackgroundTasks/types/CheckOrderInvoicesStatus.ts
similarity index 92%
rename from src/orders/types/CheckOrderInvoicesStatus.ts
rename to src/containers/BackgroundTasks/types/CheckOrderInvoicesStatus.ts
index 85dc7962e..848f69b04 100644
--- a/src/orders/types/CheckOrderInvoicesStatus.ts
+++ b/src/containers/BackgroundTasks/types/CheckOrderInvoicesStatus.ts
@@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
-import { JobStatusEnum } from "./../../types/globalTypes";
+import { JobStatusEnum } from "./../../../types/globalTypes";
// ====================================================
// GraphQL query operation: CheckOrderInvoicesStatus
diff --git a/src/fragments/errors.ts b/src/fragments/errors.ts
index 0d5eb4896..fda532572 100644
--- a/src/fragments/errors.ts
+++ b/src/fragments/errors.ts
@@ -120,3 +120,10 @@ export const appErrorFragment = gql`
permissions
}
`;
+
+export const exportErrorFragment = gql`
+ fragment ExportErrorFragment on ExportError {
+ code
+ field
+ }
+`;
diff --git a/src/fragments/products.ts b/src/fragments/products.ts
index 9cd61d0d7..234384eba 100644
--- a/src/fragments/products.ts
+++ b/src/fragments/products.ts
@@ -241,3 +241,11 @@ export const fragmentVariant = gql`
}
}
`;
+
+export const exportFileFragment = gql`
+ fragment ExportFileFragment on ExportFile {
+ id
+ status
+ url
+ }
+`;
diff --git a/src/fragments/types/ExportErrorFragment.ts b/src/fragments/types/ExportErrorFragment.ts
new file mode 100644
index 000000000..7b421283b
--- /dev/null
+++ b/src/fragments/types/ExportErrorFragment.ts
@@ -0,0 +1,15 @@
+/* tslint:disable */
+/* eslint-disable */
+// This file was automatically generated and should not be edited.
+
+import { ExportErrorCode } from "./../../types/globalTypes";
+
+// ====================================================
+// GraphQL fragment: ExportErrorFragment
+// ====================================================
+
+export interface ExportErrorFragment {
+ __typename: "ExportError";
+ code: ExportErrorCode;
+ field: string | null;
+}
diff --git a/src/fragments/types/ExportFileFragment.ts b/src/fragments/types/ExportFileFragment.ts
new file mode 100644
index 000000000..ebc4ae1d6
--- /dev/null
+++ b/src/fragments/types/ExportFileFragment.ts
@@ -0,0 +1,16 @@
+/* tslint:disable */
+/* eslint-disable */
+// This file was automatically generated and should not be edited.
+
+import { JobStatusEnum } from "./../../types/globalTypes";
+
+// ====================================================
+// GraphQL fragment: ExportFileFragment
+// ====================================================
+
+export interface ExportFileFragment {
+ __typename: "ExportFile";
+ id: string;
+ status: JobStatusEnum;
+ url: string | null;
+}
diff --git a/src/intl.ts b/src/intl.ts
index 3dc004777..1b8042ec8 100644
--- a/src/intl.ts
+++ b/src/intl.ts
@@ -132,6 +132,10 @@ export const buttonMessages = defineMessages({
defaultMessage: "Manage",
description: "button"
},
+ nextStep: {
+ defaultMessage: "Next",
+ description: "go to next step, button"
+ },
ok: {
defaultMessage: "OK",
description: "button"
@@ -144,6 +148,10 @@ export const buttonMessages = defineMessages({
defaultMessage: "Save",
description: "button"
},
+ selectAll: {
+ defaultMessage: "Select All",
+ description: "select all options, button"
+ },
send: {
defaultMessage: "Send",
description: "button"
diff --git a/src/orders/queries.ts b/src/orders/queries.ts
index a0d227c23..334c15538 100644
--- a/src/orders/queries.ts
+++ b/src/orders/queries.ts
@@ -1,8 +1,5 @@
import { fragmentAddress } from "@saleor/fragments/address";
-import {
- fragmentOrderDetails,
- invoiceFragment
-} from "@saleor/fragments/orders";
+import { fragmentOrderDetails } from "@saleor/fragments/orders";
import makeQuery from "@saleor/hooks/makeQuery";
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import gql from "graphql-tag";
@@ -231,15 +228,3 @@ export const useOrderFulfillData = makeQuery<
OrderFulfillData,
OrderFulfillDataVariables
>(orderFulfillData);
-
-export const checkOrderInvoicesStatus = gql`
- ${invoiceFragment}
- query CheckOrderInvoicesStatus($id: ID!) {
- order(id: $id) {
- id
- invoices {
- ...InvoiceFragment
- }
- }
- }
-`;
diff --git a/src/orders/views/OrderDetails/OrderDetailsMessages.tsx b/src/orders/views/OrderDetails/OrderDetailsMessages.tsx
index 56b3112b8..f3ff65e5e 100644
--- a/src/orders/views/OrderDetails/OrderDetailsMessages.tsx
+++ b/src/orders/views/OrderDetails/OrderDetailsMessages.tsx
@@ -1,4 +1,4 @@
-import { messages } from "@saleor/containers/BackgroundTasks/tasks";
+import messages from "@saleor/containers/BackgroundTasks/messages";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
diff --git a/src/productTypes/components/AssignAttributeDialog/AssignAttributeDialog.tsx b/src/productTypes/components/AssignAttributeDialog/AssignAttributeDialog.tsx
index 4d73b6f2a..32040d27b 100644
--- a/src/productTypes/components/AssignAttributeDialog/AssignAttributeDialog.tsx
+++ b/src/productTypes/components/AssignAttributeDialog/AssignAttributeDialog.tsx
@@ -30,7 +30,7 @@ import React from "react";
import InfiniteScroll from "react-infinite-scroller";
import { FormattedMessage, useIntl } from "react-intl";
-import { SearchAttributes_productType_availableAttributes_edges_node } from "../../hooks/useAvailableAttributeSearch/types/SearchAttributes";
+import { SearchAvailableAttributes_productType_availableAttributes_edges_node } from "../../hooks/useAvailableAttributeSearch/types/SearchAvailableAttributes";
const useStyles = makeStyles(
theme => ({
@@ -63,7 +63,7 @@ export interface AssignAttributeDialogProps extends FetchMoreProps {
confirmButtonState: ConfirmButtonTransitionState;
errors: string[];
open: boolean;
- attributes: SearchAttributes_productType_availableAttributes_edges_node[];
+ attributes: SearchAvailableAttributes_productType_availableAttributes_edges_node[];
selected: string[];
onClose: () => void;
onFetch: (query: string) => void;
diff --git a/src/productTypes/components/ProductTypeAttributeEditDialog/ProductTypeAttributeEditDialog.tsx b/src/productTypes/components/ProductTypeAttributeEditDialog/ProductTypeAttributeEditDialog.tsx
deleted file mode 100644
index ab82300e1..000000000
--- a/src/productTypes/components/ProductTypeAttributeEditDialog/ProductTypeAttributeEditDialog.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import Button from "@material-ui/core/Button";
-import Dialog from "@material-ui/core/Dialog";
-import DialogActions from "@material-ui/core/DialogActions";
-import DialogContent from "@material-ui/core/DialogContent";
-import DialogTitle from "@material-ui/core/DialogTitle";
-import TextField from "@material-ui/core/TextField";
-import Form from "@saleor/components/Form";
-import { FormSpacer } from "@saleor/components/FormSpacer";
-import ListField from "@saleor/components/ListField";
-import { buttonMessages } from "@saleor/intl";
-import { UserError } from "@saleor/types";
-import { getFieldError } from "@saleor/utils/errors";
-import React from "react";
-import { FormattedMessage, useIntl } from "react-intl";
-
-export interface FormData {
- name: string;
- values: Array<{
- label: string;
- value: string;
- }>;
-}
-
-export interface ProductTypeAttributeEditDialogProps {
- disabled: boolean;
- errors: UserError[];
- name: string;
- opened: boolean;
- title: string;
- values: Array<{
- label: string;
- value: string;
- }>;
- onClose: () => void;
- onConfirm: (data: FormData) => void;
-}
-
-const ProductTypeAttributeEditDialog: React.FC = ({
- disabled,
- errors,
- name,
- opened,
- title,
- values,
- onClose,
- onConfirm
-}) => {
- const intl = useIntl();
-
- const initialForm: FormData = {
- name: name || "",
- values: values || []
- };
- return (
-
- );
-};
-ProductTypeAttributeEditDialog.displayName = "ProductTypeAttributeEditDialog";
-export default ProductTypeAttributeEditDialog;
diff --git a/src/productTypes/components/ProductTypeAttributeEditDialog/index.ts b/src/productTypes/components/ProductTypeAttributeEditDialog/index.ts
deleted file mode 100644
index 6344b380f..000000000
--- a/src/productTypes/components/ProductTypeAttributeEditDialog/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default } from "./ProductTypeAttributeEditDialog";
-export * from "./ProductTypeAttributeEditDialog";
diff --git a/src/productTypes/hooks/useAvailableAttributeSearch/index.tsx b/src/productTypes/hooks/useAvailableAttributeSearch/index.tsx
index 0aa5830da..bbc26d5e8 100644
--- a/src/productTypes/hooks/useAvailableAttributeSearch/index.tsx
+++ b/src/productTypes/hooks/useAvailableAttributeSearch/index.tsx
@@ -3,13 +3,13 @@ import makeSearch from "@saleor/hooks/makeSearch";
import gql from "graphql-tag";
import {
- SearchAttributes,
- SearchAttributesVariables
-} from "./types/SearchAttributes";
+ SearchAvailableAttributes,
+ SearchAvailableAttributesVariables
+} from "./types/SearchAvailableAttributes";
export const searchAttributes = gql`
${pageInfoFragment}
- query SearchAttributes(
+ query SearchAvailableAttributes(
$id: ID!
$after: String
$first: Int!
@@ -37,35 +37,36 @@ export const searchAttributes = gql`
}
`;
-export default makeSearch(
- searchAttributes,
- result =>
- result.loadMore(
- (prev, next) => {
- if (
- prev.productType.availableAttributes.pageInfo.endCursor ===
- next.productType.availableAttributes.pageInfo.endCursor
- ) {
- return prev;
- }
-
- return {
- ...prev,
- productType: {
- ...prev.productType,
- availableAttributes: {
- ...prev.productType.availableAttributes,
- edges: [
- ...prev.productType.availableAttributes.edges,
- ...next.productType.availableAttributes.edges
- ],
- pageInfo: next.productType.availableAttributes.pageInfo
- }
- }
- };
- },
- {
- after: result.data.productType.availableAttributes.pageInfo.endCursor
+export default makeSearch<
+ SearchAvailableAttributes,
+ SearchAvailableAttributesVariables
+>(searchAttributes, result =>
+ result.loadMore(
+ (prev, next) => {
+ if (
+ prev.productType.availableAttributes.pageInfo.endCursor ===
+ next.productType.availableAttributes.pageInfo.endCursor
+ ) {
+ return prev;
}
- )
+
+ return {
+ ...prev,
+ productType: {
+ ...prev.productType,
+ availableAttributes: {
+ ...prev.productType.availableAttributes,
+ edges: [
+ ...prev.productType.availableAttributes.edges,
+ ...next.productType.availableAttributes.edges
+ ],
+ pageInfo: next.productType.availableAttributes.pageInfo
+ }
+ }
+ };
+ },
+ {
+ after: result.data.productType.availableAttributes.pageInfo.endCursor
+ }
+ )
);
diff --git a/src/productTypes/hooks/useAvailableAttributeSearch/types/SearchAttributes.ts b/src/productTypes/hooks/useAvailableAttributeSearch/types/SearchAttributes.ts
deleted file mode 100644
index b45411fa9..000000000
--- a/src/productTypes/hooks/useAvailableAttributeSearch/types/SearchAttributes.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/* tslint:disable */
-/* eslint-disable */
-// This file was automatically generated and should not be edited.
-
-// ====================================================
-// GraphQL query operation: SearchAttributes
-// ====================================================
-
-export interface SearchAttributes_productType_availableAttributes_edges_node {
- __typename: "Attribute";
- id: string;
- name: string | null;
- slug: string | null;
-}
-
-export interface SearchAttributes_productType_availableAttributes_edges {
- __typename: "AttributeCountableEdge";
- node: SearchAttributes_productType_availableAttributes_edges_node;
-}
-
-export interface SearchAttributes_productType_availableAttributes_pageInfo {
- __typename: "PageInfo";
- endCursor: string | null;
- hasNextPage: boolean;
- hasPreviousPage: boolean;
- startCursor: string | null;
-}
-
-export interface SearchAttributes_productType_availableAttributes {
- __typename: "AttributeCountableConnection";
- edges: SearchAttributes_productType_availableAttributes_edges[];
- pageInfo: SearchAttributes_productType_availableAttributes_pageInfo;
-}
-
-export interface SearchAttributes_productType {
- __typename: "ProductType";
- id: string;
- availableAttributes: SearchAttributes_productType_availableAttributes | null;
-}
-
-export interface SearchAttributes {
- productType: SearchAttributes_productType | null;
-}
-
-export interface SearchAttributesVariables {
- id: string;
- after?: string | null;
- first: number;
- query: string;
-}
diff --git a/src/productTypes/hooks/useAvailableAttributeSearch/types/SearchAvailableAttributes.ts b/src/productTypes/hooks/useAvailableAttributeSearch/types/SearchAvailableAttributes.ts
new file mode 100644
index 000000000..9956975e7
--- /dev/null
+++ b/src/productTypes/hooks/useAvailableAttributeSearch/types/SearchAvailableAttributes.ts
@@ -0,0 +1,50 @@
+/* tslint:disable */
+/* eslint-disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: SearchAvailableAttributes
+// ====================================================
+
+export interface SearchAvailableAttributes_productType_availableAttributes_edges_node {
+ __typename: "Attribute";
+ id: string;
+ name: string | null;
+ slug: string | null;
+}
+
+export interface SearchAvailableAttributes_productType_availableAttributes_edges {
+ __typename: "AttributeCountableEdge";
+ node: SearchAvailableAttributes_productType_availableAttributes_edges_node;
+}
+
+export interface SearchAvailableAttributes_productType_availableAttributes_pageInfo {
+ __typename: "PageInfo";
+ endCursor: string | null;
+ hasNextPage: boolean;
+ hasPreviousPage: boolean;
+ startCursor: string | null;
+}
+
+export interface SearchAvailableAttributes_productType_availableAttributes {
+ __typename: "AttributeCountableConnection";
+ edges: SearchAvailableAttributes_productType_availableAttributes_edges[];
+ pageInfo: SearchAvailableAttributes_productType_availableAttributes_pageInfo;
+}
+
+export interface SearchAvailableAttributes_productType {
+ __typename: "ProductType";
+ id: string;
+ availableAttributes: SearchAvailableAttributes_productType_availableAttributes | null;
+}
+
+export interface SearchAvailableAttributes {
+ productType: SearchAvailableAttributes_productType | null;
+}
+
+export interface SearchAvailableAttributesVariables {
+ id: string;
+ after?: string | null;
+ first: number;
+ query: string;
+}
diff --git a/src/products/components/ProductExportDialog/ProductExportDialog.stories.tsx b/src/products/components/ProductExportDialog/ProductExportDialog.stories.tsx
new file mode 100644
index 000000000..734452efd
--- /dev/null
+++ b/src/products/components/ProductExportDialog/ProductExportDialog.stories.tsx
@@ -0,0 +1,53 @@
+import Decorator from "@saleor/storybook/Decorator";
+import {
+ ExportErrorCode,
+ ExportProductsInput
+} from "@saleor/types/globalTypes";
+import { storiesOf } from "@storybook/react";
+import React from "react";
+
+import { attributes } from "../../../attributes/fixtures";
+import ProductExportDialog, {
+ ProductExportDialogProps
+} from "./ProductExportDialog";
+
+const props: ProductExportDialogProps = {
+ attributes: attributes.map(attr => ({
+ __typename: "Attribute",
+ id: attr.id,
+ name: attr.name
+ })),
+ confirmButtonState: "default",
+ errors: [],
+ hasMore: true,
+ loading: true,
+ onClose: () => undefined,
+ onFetch: () => undefined,
+ onFetchMore: () => undefined,
+ onSubmit: () => undefined,
+ open: true,
+ productQuantity: {
+ all: 100,
+ filter: 32
+ },
+ selectedProducts: 18
+};
+
+storiesOf("Views / Products / Export / Export settings", module)
+ .addDecorator(Decorator)
+ .add("interactive", () => )
+ .add("no products selected", () => (
+
+ ))
+ .add("errors", () => (
+ ).map(field => ({
+ __typename: "ExportError",
+ code: ExportErrorCode.INVALID,
+ field
+ }))}
+ />
+ ));
diff --git a/src/products/components/ProductExportDialog/ProductExportDialog.tsx b/src/products/components/ProductExportDialog/ProductExportDialog.tsx
new file mode 100644
index 000000000..32a1d218b
--- /dev/null
+++ b/src/products/components/ProductExportDialog/ProductExportDialog.tsx
@@ -0,0 +1,227 @@
+import Button from "@material-ui/core/Button";
+import Dialog from "@material-ui/core/Dialog";
+import DialogActions from "@material-ui/core/DialogActions";
+import DialogContent from "@material-ui/core/DialogContent";
+import DialogTitle from "@material-ui/core/DialogTitle";
+import Typography from "@material-ui/core/Typography";
+import ConfirmButton, {
+ ConfirmButtonTransitionState
+} from "@saleor/components/ConfirmButton";
+import makeCreatorSteps, { Step } from "@saleor/components/CreatorSteps";
+import Form from "@saleor/components/Form";
+import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
+import { ExportErrorFragment } from "@saleor/fragments/types/ExportErrorFragment";
+import { FormChange } from "@saleor/hooks/useForm";
+import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
+import useWizard from "@saleor/hooks/useWizard";
+import { buttonMessages } from "@saleor/intl";
+import { SearchAttributes_search_edges_node } from "@saleor/searches/types/SearchAttributes";
+import { DialogProps, FetchMoreProps } from "@saleor/types";
+import {
+ ExportProductsInput,
+ ExportScope,
+ FileTypesEnum
+} from "@saleor/types/globalTypes";
+import getExportErrorMessage from "@saleor/utils/errors/export";
+import { toggle } from "@saleor/utils/lists";
+import { mapNodeToChoice } from "@saleor/utils/maps";
+import React from "react";
+import { FormattedMessage, useIntl } from "react-intl";
+
+import ProductExportDialogInfo, {
+ attributeNamePrefix
+} from "./ProductExportDialogInfo";
+import ProductExportDialogSettings, {
+ ProductQuantity
+} from "./ProductExportDialogSettings";
+
+export enum ProductExportStep {
+ INFO,
+ SETTINGS
+}
+
+function useSteps(): Array> {
+ const intl = useIntl();
+
+ return [
+ {
+ label: intl.formatMessage({
+ defaultMessage: "Information exported",
+ description: "product export to csv file, header"
+ }),
+ value: ProductExportStep.INFO
+ },
+ {
+ label: intl.formatMessage({
+ defaultMessage: "Export Settings",
+ description: "product export to csv file, header"
+ }),
+ value: ProductExportStep.SETTINGS
+ }
+ ];
+}
+
+const initialForm: ExportProductsInput = {
+ exportInfo: {
+ attributes: [],
+ fields: []
+ },
+ fileType: FileTypesEnum.CSV,
+ scope: ExportScope.ALL
+};
+
+const ProductExportSteps = makeCreatorSteps();
+
+export interface ProductExportDialogProps extends DialogProps, FetchMoreProps {
+ attributes: SearchAttributes_search_edges_node[];
+ confirmButtonState: ConfirmButtonTransitionState;
+ errors: ExportErrorFragment[];
+ productQuantity: ProductQuantity;
+ selectedProducts: number;
+ onFetch: (query: string) => void;
+ onSubmit: (data: ExportProductsInput) => void;
+}
+
+const ProductExportDialog: React.FC = ({
+ attributes,
+ confirmButtonState,
+ errors,
+ productQuantity,
+ onClose,
+ onSubmit,
+ open,
+ selectedProducts,
+ ...fetchMoreProps
+}) => {
+ const [step, { next, prev, set: setStep }] = useWizard(
+ ProductExportStep.INFO,
+ [ProductExportStep.INFO, ProductExportStep.SETTINGS]
+ );
+ const steps = useSteps();
+ const dialogErrors = useModalDialogErrors(errors, open);
+ const notFormErrors = dialogErrors.filter(err => !err.field);
+ const intl = useIntl();
+ const [selectedAttributes, setSelectedAttributes] = React.useState<
+ MultiAutocompleteChoiceType[]
+ >([]);
+
+ const attributeChoices = mapNodeToChoice(attributes);
+
+ return (
+
+ );
+};
+
+ProductExportDialog.displayName = "ProductExportDialog";
+export default ProductExportDialog;
diff --git a/src/products/components/ProductExportDialog/ProductExportDialogInfo.tsx b/src/products/components/ProductExportDialog/ProductExportDialogInfo.tsx
new file mode 100644
index 000000000..991137265
--- /dev/null
+++ b/src/products/components/ProductExportDialog/ProductExportDialogInfo.tsx
@@ -0,0 +1,448 @@
+import Button from "@material-ui/core/Button";
+import Checkbox from "@material-ui/core/Checkbox";
+import CircularProgress from "@material-ui/core/CircularProgress";
+import FormControlLabel from "@material-ui/core/FormControlLabel";
+import makeStyles from "@material-ui/core/styles/makeStyles";
+import TextField from "@material-ui/core/TextField";
+import Typography from "@material-ui/core/Typography";
+import Accordion, { AccordionProps } from "@saleor/components/Accordion";
+import Chip from "@saleor/components/Chip";
+import Hr from "@saleor/components/Hr";
+import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
+import { ChangeEvent } from "@saleor/hooks/useForm";
+import useSearchQuery from "@saleor/hooks/useSearchQuery";
+import { sectionNames } from "@saleor/intl";
+import { FetchMoreProps } from "@saleor/types";
+import {
+ ExportProductsInput,
+ ProductFieldEnum
+} from "@saleor/types/globalTypes";
+import { toggle } from "@saleor/utils/lists";
+import React from "react";
+import { useIntl } from "react-intl";
+import { FormattedMessage } from "react-intl";
+
+export const attributeNamePrefix = "attribute-";
+const maxChips = 4;
+
+const useStyles = makeStyles(
+ theme => ({
+ accordion: {
+ marginBottom: theme.spacing(2)
+ },
+ checkbox: {
+ position: "relative",
+ right: -theme.spacing(1.5)
+ },
+ chip: {
+ marginBottom: theme.spacing(1),
+ marginRight: theme.spacing()
+ },
+ dialogLabel: {
+ marginBottom: theme.spacing(2)
+ },
+ hr: {
+ marginBottom: theme.spacing(3),
+ marginTop: theme.spacing(3)
+ },
+ label: {
+ "&&": {
+ overflow: "visible"
+ },
+ justifyContent: "space-between",
+ margin: theme.spacing(0),
+ width: "100%"
+ },
+ loadMoreContainer: {
+ display: "flex",
+ justifyContent: "center",
+ marginTop: theme.spacing(2)
+ },
+ moreLabel: {
+ display: "inline-block",
+ marginBottom: theme.spacing()
+ },
+ quickPeekContainer: {
+ marginBottom: theme.spacing(-1)
+ }
+ }),
+ {
+ name: "ProductExportDialogInfo"
+ }
+);
+
+const Option: React.FC<{
+ checked: boolean;
+ name: string;
+ onChange: (event: ChangeEvent) => void;
+}> = ({ checked, children, name, onChange }) => {
+ const classes = useStyles({});
+
+ return (
+
+ }
+ className={classes.label}
+ label={children}
+ labelPlacement="start"
+ >
+ );
+};
+
+const FieldAccordion: React.FC void;
+ onToggleAll: (field: ProductFieldEnum[], setTo: boolean) => void;
+}> = ({ data, fields, onChange, onToggleAll, ...props }) => {
+ const classes = useStyles({});
+ const intl = useIntl();
+
+ const fieldNames: Record = {
+ [ProductFieldEnum.CATEGORY]: intl.formatMessage({
+ defaultMessage: "Category",
+ description: "product field"
+ }),
+ [ProductFieldEnum.CHARGE_TAXES]: intl.formatMessage({
+ defaultMessage: "Charge Taxes",
+ description: "product field"
+ }),
+ [ProductFieldEnum.COLLECTIONS]: intl.formatMessage({
+ defaultMessage: "Collections",
+ description: "product field"
+ }),
+ [ProductFieldEnum.COST_PRICE]: intl.formatMessage({
+ defaultMessage: "Cost Price",
+ description: "product field"
+ }),
+ [ProductFieldEnum.DESCRIPTION]: intl.formatMessage({
+ defaultMessage: "Description",
+ description: "product field"
+ }),
+ [ProductFieldEnum.NAME]: intl.formatMessage({
+ defaultMessage: "Name",
+ description: "product field"
+ }),
+ [ProductFieldEnum.PRODUCT_IMAGES]: intl.formatMessage({
+ defaultMessage: "Product Images",
+ description: "product field"
+ }),
+ [ProductFieldEnum.PRODUCT_TYPE]: intl.formatMessage({
+ defaultMessage: "Type",
+ description: "product field"
+ }),
+ [ProductFieldEnum.PRODUCT_WEIGHT]: intl.formatMessage({
+ defaultMessage: "Weight",
+ description: "product field"
+ }),
+ [ProductFieldEnum.VARIANT_IMAGES]: intl.formatMessage({
+ defaultMessage: "Variant Images",
+ description: "product field"
+ }),
+ [ProductFieldEnum.VARIANT_PRICE]: intl.formatMessage({
+ defaultMessage: "Variant Price",
+ description: "product field"
+ }),
+ [ProductFieldEnum.VARIANT_SKU]: intl.formatMessage({
+ defaultMessage: "SKU",
+ description: "product field"
+ }),
+ [ProductFieldEnum.VARIANT_WEIGHT]: intl.formatMessage({
+ defaultMessage: "Variant Weight",
+ description: "product field"
+ }),
+ [ProductFieldEnum.VISIBLE]: intl.formatMessage({
+ defaultMessage: "Visibility",
+ description: "product field"
+ })
+ };
+
+ const selectedAll = fields.every(field =>
+ data.exportInfo.fields.includes(field)
+ );
+
+ const selectedFields = data.exportInfo.fields.filter(field =>
+ fields.includes(field)
+ );
+
+ return (
+ 0 && (
+
+ {selectedFields.slice(0, maxChips).map(field => (
+
+ onChange({
+ target: {
+ name: field,
+ value: false
+ }
+ })
+ }
+ />
+ ))}
+ {selectedFields.length > maxChips && (
+
+
+
+ )}
+
+ )
+ }
+ {...props}
+ >
+
+ {fields.map(field => (
+
+ ))}
+
+ );
+};
+
+export interface ProductExportDialogInfoProps extends FetchMoreProps {
+ attributes: MultiAutocompleteChoiceType[];
+ data: ExportProductsInput;
+ selectedAttributes: MultiAutocompleteChoiceType[];
+ onAttrtibuteSelect: (event: ChangeEvent) => void;
+ onChange: (event: ChangeEvent) => void;
+ onFetch: (query: string) => void;
+}
+
+const ProductExportDialogInfo: React.FC = ({
+ attributes,
+ data,
+ hasMore,
+ selectedAttributes,
+ loading,
+ onAttrtibuteSelect,
+ onChange,
+ onFetch,
+ onFetchMore
+}) => {
+ const classes = useStyles({});
+ const intl = useIntl();
+ const [query, onQueryChange] = useSearchQuery(onFetch);
+
+ const handleFieldChange = (event: ChangeEvent) =>
+ onChange({
+ target: {
+ name: "exportInfo",
+ value: {
+ ...data.exportInfo,
+ fields: toggle(
+ event.target.name,
+ data.exportInfo.fields,
+ (a, b) => a === b
+ )
+ }
+ }
+ });
+
+ const handleToggleAllFields = (fields: ProductFieldEnum[], setTo: boolean) =>
+ onChange({
+ target: {
+ name: "exportInfo",
+ value: {
+ ...data.exportInfo,
+ fields: setTo
+ ? [
+ ...data.exportInfo.fields,
+ ...fields.filter(
+ field => !data.exportInfo.fields.includes(field)
+ )
+ ]
+ : data.exportInfo.fields.filter(field => !fields.includes(field))
+ }
+ }
+ });
+
+ return (
+ <>
+
+
+
+
+ 0 && (
+
+ {selectedAttributes.slice(0, maxChips).map(attribute => (
+
+ onAttrtibuteSelect({
+ target: {
+ name: attributeNamePrefix + attribute.value,
+ value: undefined
+ }
+ })
+ }
+ />
+ ))}
+ {selectedAttributes.length > maxChips && (
+
+
+
+ )}
+
+ )
+ }
+ data-test="attributes"
+ >
+
+ }}
+ />
+
+ {attributes.map(attribute => (
+
+ ))}
+ {(hasMore || loading) && (
+
+ {hasMore && !loading && (
+
+ )}
+ {loading && }
+
+ )}
+
+
+
+
+ >
+ );
+};
+
+ProductExportDialogInfo.displayName = "ProductExportDialogInfo";
+export default ProductExportDialogInfo;
diff --git a/src/products/components/ProductExportDialog/ProductExportDialogSettings.tsx b/src/products/components/ProductExportDialog/ProductExportDialogSettings.tsx
new file mode 100644
index 000000000..040b0dbc3
--- /dev/null
+++ b/src/products/components/ProductExportDialog/ProductExportDialogSettings.tsx
@@ -0,0 +1,144 @@
+import makeStyles from "@material-ui/core/styles/makeStyles";
+import Hr from "@saleor/components/Hr";
+import RadioGroupField, {
+ RadioGroupFieldChoice
+} from "@saleor/components/RadioGroupField";
+import { ExportErrorFragment } from "@saleor/fragments/types/ExportErrorFragment";
+import { ChangeEvent } from "@saleor/hooks/useForm";
+import {
+ ExportProductsInput,
+ ExportScope,
+ FileTypesEnum
+} from "@saleor/types/globalTypes";
+import { getFormErrors } from "@saleor/utils/errors";
+import getExportErrorMessage from "@saleor/utils/errors/export";
+import React from "react";
+import { useIntl } from "react-intl";
+
+const useStyles = makeStyles(
+ theme => ({
+ hr: {
+ marginBottom: theme.spacing(3),
+ marginTop: theme.spacing(3)
+ }
+ }),
+ {
+ name: "ProductExportDialogSettings"
+ }
+);
+
+export type ProductQuantity = Record<"all" | "filter", number>;
+export interface ProductExportDialogSettingsProps {
+ data: ExportProductsInput;
+ errors: ExportErrorFragment[];
+ productQuantity: ProductQuantity;
+ selectedProducts: number;
+ onChange: (event: ChangeEvent) => void;
+}
+
+const formFields: Array = ["fileType", "scope"];
+
+const ProductExportDialogSettings: React.FC = ({
+ data,
+ errors,
+ onChange,
+ productQuantity,
+ selectedProducts
+}) => {
+ const classes = useStyles({});
+ const intl = useIntl();
+
+ const formErrors = getFormErrors(formFields, errors);
+
+ const productsToExportChoices: Array> = [
+ {
+ label: intl.formatMessage(
+ {
+ defaultMessage: "All products ({number})",
+ description: "export all products to csv file"
+ },
+ {
+ number: productQuantity.all || "..."
+ }
+ ),
+ value: ExportScope.ALL
+ },
+ {
+ disabled: selectedProducts === 0,
+ label: intl.formatMessage(
+ {
+ defaultMessage: "Selected products ({number})",
+ description: "export selected products to csv file"
+ },
+ {
+ number: selectedProducts
+ }
+ ),
+ value: ExportScope.IDS
+ },
+ {
+ label: intl.formatMessage(
+ {
+ defaultMessage: "Current search ({number})",
+ description: "export filtered products to csv file"
+ },
+ {
+ number: productQuantity.filter || "..."
+ }
+ ),
+ value: ExportScope.FILTER
+ }
+ ];
+
+ const productExportTypeChoices: Array> = [
+ {
+ label: intl.formatMessage({
+ defaultMessage: "Spreadsheet for Excel, Numbers etc.",
+ description: "export products as spreadsheet"
+ }),
+ value: FileTypesEnum.XLSX
+ },
+ {
+ label: intl.formatMessage({
+ defaultMessage: "Plain CSV file",
+ description: "export products as csv file"
+ }),
+ value: FileTypesEnum.CSV
+ }
+ ];
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+ProductExportDialogSettings.displayName = "ProductExportDialogSettings";
+export default ProductExportDialogSettings;
diff --git a/src/products/components/ProductExportDialog/index.ts b/src/products/components/ProductExportDialog/index.ts
new file mode 100644
index 000000000..ea211ddcc
--- /dev/null
+++ b/src/products/components/ProductExportDialog/index.ts
@@ -0,0 +1,2 @@
+export * from "./ProductExportDialog";
+export { default } from "./ProductExportDialog";
diff --git a/src/products/components/ProductListPage/ProductListPage.tsx b/src/products/components/ProductListPage/ProductListPage.tsx
index 6652fc6bd..8bc6b452c 100644
--- a/src/products/components/ProductListPage/ProductListPage.tsx
+++ b/src/products/components/ProductListPage/ProductListPage.tsx
@@ -1,6 +1,7 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import makeStyles from "@material-ui/core/styles/makeStyles";
+import CardMenu from "@saleor/components/CardMenu";
import ColumnPicker, {
ColumnPickerChoice
} from "@saleor/components/ColumnPicker";
@@ -44,12 +45,13 @@ export interface ProductListPageProps
gridAttributes: GridAttributes_grid_edges_node[];
totalGridAttributes: number;
products: ProductList_products_edges_node[];
+ onExport: () => void;
}
const useStyles = makeStyles(
theme => ({
columnPicker: {
- marginRight: theme.spacing(3)
+ margin: theme.spacing(0, 3)
}
}),
{ name: "ProductListPage" }
@@ -71,6 +73,7 @@ export const ProductListPage: React.FC = props => {
totalGridAttributes,
onAdd,
onAll,
+ onExport,
onFetchMore,
onFilterChange,
onSearchChange,
@@ -119,6 +122,19 @@ export const ProductListPage: React.FC = props => {
return (
+
> {
+ const intl = useIntl();
+
return [
{
label: intl.formatMessage({
@@ -36,69 +32,27 @@ function getSteps(intl: IntlShape): Step[] {
];
}
-const useStyles = makeStyles(
- theme => ({
- label: {
- fontSize: 14,
- textAlign: "center"
- },
- root: {
- borderBottom: `1px solid ${theme.palette.divider}`,
- display: "flex",
- justifyContent: "space-between",
- marginBottom: theme.spacing(3)
- },
- tab: {
- flex: 1,
- paddingBottom: theme.spacing(),
- userSelect: "none"
- },
- tabActive: {
- fontWeight: 600
- },
- tabVisited: {
- borderBottom: `3px solid ${theme.palette.primary.main}`,
- cursor: "pointer"
- }
- }),
- {
- name: "ProductVariantCreatorTabs"
- }
-);
+const ProductVariantCreatorSteps = makeCreatorSteps<
+ ProductVariantCreatorStep
+>();
export interface ProductVariantCreatorTabsProps {
step: ProductVariantCreatorStep;
onStepClick: (step: ProductVariantCreatorStep) => void;
}
-const ProductVariantCreatorTabs: React.FC = props => {
- const { step: currentStep, onStepClick } = props;
- const classes = useStyles(props);
- const intl = useIntl();
- const steps = getSteps(intl);
+const ProductVariantCreatorTabs: React.FC = ({
+ step: currentStep,
+ onStepClick
+}) => {
+ const steps = useSteps();
return (
-
- {steps.map((step, stepIndex) => {
- const visitedStep =
- steps.findIndex(step => step.value === currentStep) >= stepIndex;
-
- return (
-
onStepClick(step.value) : undefined}
- key={step.value}
- >
-
- {step.label}
-
-
- );
- })}
-
+
);
};
diff --git a/src/products/mutations.ts b/src/products/mutations.ts
index 255d8d1c7..7a8927b9e 100644
--- a/src/products/mutations.ts
+++ b/src/products/mutations.ts
@@ -1,10 +1,12 @@
import {
bulkProductErrorFragment,
bulkStockErrorFragment,
+ exportErrorFragment,
productErrorFragment,
stockErrorFragment
} from "@saleor/fragments/errors";
import {
+ exportFileFragment,
fragmentVariant,
productFragmentDetails
} from "@saleor/fragments/products";
@@ -22,6 +24,7 @@ import {
} from "./types/productBulkPublish";
import { ProductCreate, ProductCreateVariables } from "./types/ProductCreate";
import { ProductDelete, ProductDeleteVariables } from "./types/ProductDelete";
+import { ProductExport, ProductExportVariables } from "./types/ProductExport";
import {
ProductImageCreate,
ProductImageCreateVariables
@@ -567,3 +570,22 @@ export const TypedProductVariantBulkDeleteMutation = TypedMutation<
ProductVariantBulkDelete,
ProductVariantBulkDeleteVariables
>(ProductVariantBulkDeleteMutation);
+
+export const productExportMutation = gql`
+ ${exportFileFragment}
+ ${exportErrorFragment}
+ mutation ProductExport($input: ExportProductsInput!) {
+ exportProducts(input: $input) {
+ exportFile {
+ ...ExportFileFragment
+ }
+ errors: exportErrors {
+ ...ExportErrorFragment
+ }
+ }
+ }
+`;
+export const useProductExport = makeMutation<
+ ProductExport,
+ ProductExportVariables
+>(productExportMutation);
diff --git a/src/products/queries.ts b/src/products/queries.ts
index 017fd0bf7..80d5d8a1f 100644
--- a/src/products/queries.ts
+++ b/src/products/queries.ts
@@ -11,6 +11,7 @@ import makeQuery from "@saleor/hooks/makeQuery";
import gql from "graphql-tag";
import { TypedQuery } from "../queries";
+import { CountAllProducts } from "./types/CountAllProducts";
import {
CreateMultipleVariantsData,
CreateMultipleVariantsDataVariables
@@ -145,6 +146,7 @@ const productListQuery = gql`
startCursor
endCursor
}
+ totalCount
}
}
`;
@@ -153,6 +155,17 @@ export const TypedProductListQuery = TypedQuery<
ProductListVariables
>(productListQuery);
+const countAllProductsQuery = gql`
+ query CountAllProducts {
+ products {
+ totalCount
+ }
+ }
+`;
+export const useCountAllProducts = makeQuery(
+ countAllProductsQuery
+);
+
const productDetailsQuery = gql`
${productFragmentDetails}
query ProductDetails($id: ID!) {
diff --git a/src/products/types/CountAllProducts.ts b/src/products/types/CountAllProducts.ts
new file mode 100644
index 000000000..1962d82e3
--- /dev/null
+++ b/src/products/types/CountAllProducts.ts
@@ -0,0 +1,16 @@
+/* tslint:disable */
+/* eslint-disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: CountAllProducts
+// ====================================================
+
+export interface CountAllProducts_products {
+ __typename: "ProductCountableConnection";
+ totalCount: number | null;
+}
+
+export interface CountAllProducts {
+ products: CountAllProducts_products | null;
+}
diff --git a/src/products/types/ProductExport.ts b/src/products/types/ProductExport.ts
new file mode 100644
index 000000000..fcc17aad1
--- /dev/null
+++ b/src/products/types/ProductExport.ts
@@ -0,0 +1,36 @@
+/* tslint:disable */
+/* eslint-disable */
+// This file was automatically generated and should not be edited.
+
+import { ExportProductsInput, JobStatusEnum, ExportErrorCode } from "./../../types/globalTypes";
+
+// ====================================================
+// GraphQL mutation operation: ProductExport
+// ====================================================
+
+export interface ProductExport_exportProducts_exportFile {
+ __typename: "ExportFile";
+ id: string;
+ status: JobStatusEnum;
+ url: string | null;
+}
+
+export interface ProductExport_exportProducts_errors {
+ __typename: "ExportError";
+ code: ExportErrorCode;
+ field: string | null;
+}
+
+export interface ProductExport_exportProducts {
+ __typename: "ExportProducts";
+ exportFile: ProductExport_exportProducts_exportFile | null;
+ errors: ProductExport_exportProducts_errors[];
+}
+
+export interface ProductExport {
+ exportProducts: ProductExport_exportProducts | null;
+}
+
+export interface ProductExportVariables {
+ input: ExportProductsInput;
+}
diff --git a/src/products/types/ProductList.ts b/src/products/types/ProductList.ts
index 78ec749fb..c96e1001a 100644
--- a/src/products/types/ProductList.ts
+++ b/src/products/types/ProductList.ts
@@ -99,6 +99,7 @@ export interface ProductList_products {
__typename: "ProductCountableConnection";
edges: ProductList_products_edges[];
pageInfo: ProductList_products_pageInfo;
+ totalCount: number | null;
}
export interface ProductList {
diff --git a/src/products/urls.ts b/src/products/urls.ts
index 19bc641fc..859229d35 100644
--- a/src/products/urls.ts
+++ b/src/products/urls.ts
@@ -23,6 +23,7 @@ export type ProductListUrlDialog =
| "publish"
| "unpublish"
| "delete"
+ | "export"
| TabActionDialog;
export enum ProductListUrlFiltersEnum {
priceFrom = "priceFrom",
diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx
index e2ab7af14..8e9089ed3 100644
--- a/src/products/views/ProductList/ProductList.tsx
+++ b/src/products/views/ProductList/ProductList.tsx
@@ -13,6 +13,8 @@ import {
defaultListSettings,
ProductListColumns
} from "@saleor/config";
+import { Task } from "@saleor/containers/BackgroundTasks/types";
+import useBackgroundTask from "@saleor/hooks/useBackgroundTask";
import useBulkActions from "@saleor/hooks/useBulkActions";
import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator";
@@ -23,11 +25,13 @@ import usePaginator, {
import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl";
import { maybe } from "@saleor/misc";
+import ProductExportDialog from "@saleor/products/components/ProductExportDialog";
import {
getAttributeIdFromColumnValue,
isAttributeColumnValue
} from "@saleor/products/components/ProductListPage/utils";
import { ProductListVariables } from "@saleor/products/types/ProductList";
+import useAttributeSearch from "@saleor/searches/useAttributeSearch";
import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
@@ -41,11 +45,13 @@ import { FormattedMessage, useIntl } from "react-intl";
import ProductListPage from "../../components/ProductListPage";
import {
TypedProductBulkDeleteMutation,
- TypedProductBulkPublishMutation
+ TypedProductBulkPublishMutation,
+ useProductExport
} from "../../mutations";
import {
AvailableInGridAttributesQuery,
TypedProductListQuery,
+ useCountAllProducts,
useInitialProductFilterDataQuery
} from "../../queries";
import { productBulkDelete } from "../../types/productBulkDelete";
@@ -78,6 +84,7 @@ export const ProductList: React.FC = ({ params }) => {
const navigate = useNavigator();
const notify = useNotifier();
const paginate = usePaginator();
+ const { queue } = useBackgroundTask();
const shop = useShop();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids
@@ -111,6 +118,12 @@ export const ProductList: React.FC = ({ params }) => {
first: 5
}
});
+ const searchAttributes = useAttributeSearch({
+ variables: {
+ ...DEFAULT_INITIAL_SEARCH_DATA,
+ first: 10
+ }
+ });
React.useEffect(
() =>
@@ -137,6 +150,29 @@ export const ProductList: React.FC = ({ params }) => {
ProductListUrlDialog,
ProductListUrlQueryParams
>(navigate, productListUrl, params);
+ const countAllProducts = useCountAllProducts({});
+
+ const [exportProducts, exportProductsOpts] = useProductExport({
+ onCompleted: data => {
+ if (data.exportProducts.errors.length === 0) {
+ notify({
+ text: intl.formatMessage({
+ defaultMessage:
+ "We are currently exporting your requested CSV. As soon as it is available it will be sent to your email address"
+ }),
+ title: intl.formatMessage({
+ defaultMessage: "Exporting CSV",
+ description: "waiting for export to end, header"
+ })
+ });
+ queue(Task.EXPORT, {
+ id: data.exportProducts.exportFile.id
+ });
+ closeModal();
+ reset();
+ }
+ }
+ });
const [
changeFilters,
@@ -398,6 +434,7 @@ export const ProductList: React.FC = ({ params }) => {
onTabChange={handleTabChange}
initialSearch={params.query || ""}
tabs={getFilterTabs().map(tab => tab.name)}
+ onExport={() => openModal("export")}
/>
= ({ params }) => {
/>
+ edge.node)}
+ hasMore={
+ searchAttributes.result.data?.search.pageInfo
+ .hasNextPage
+ }
+ loading={searchAttributes.result.loading}
+ onFetch={searchAttributes.search}
+ onFetchMore={searchAttributes.loadMore}
+ open={params.action === "export"}
+ confirmButtonState={exportProductsOpts.status}
+ errors={
+ exportProductsOpts.data?.exportProducts.errors || []
+ }
+ productQuantity={{
+ all: countAllProducts.data?.products.totalCount,
+ filter: data?.products.totalCount
+ }}
+ selectedProducts={listElements.length}
+ onClose={closeModal}
+ onSubmit={data =>
+ exportProducts({
+ variables: {
+ input: {
+ ...data,
+ filter,
+ ids: listElements
+ }
+ }
+ })
+ }
+ />
(
+ searchAttributes
+);
diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap
index 055cf794b..02e94014a 100644
--- a/src/storybook/__snapshots__/Stories.test.ts.snap
+++ b/src/storybook/__snapshots__/Stories.test.ts.snap
@@ -48,6 +48,184 @@ exports[`Storyshots Discounts / Select countries default 1`] = `
/>
`;
+exports[`Storyshots Generics / Accordion default 1`] = `
+
+`;
+
+exports[`Storyshots Generics / Accordion opened 1`] = `
+
+`;
+
+exports[`Storyshots Generics / Accordion with quick peek 1`] = `
+
+`;
+
exports[`Storyshots Generics / Account Permission Groups Widget default 1`] = `
Summary
@@ -119378,6 +119556,24 @@ exports[`Storyshots Views / Products / Create product variant with errors 1`] =
`;
+exports[`Storyshots Views / Products / Export / Export settings errors 1`] = `
+
+`;
+
+exports[`Storyshots Views / Products / Export / Export settings interactive 1`] = `
+
+`;
+
+exports[`Storyshots Views / Products / Export / Export settings no products selected 1`] = `
+
+`;
+
exports[`Storyshots Views / Products / Product edit form errors 1`] = `