Add missing files

This commit is contained in:
dominik-zeglen 2019-08-09 12:17:04 +02:00
parent 80371c0c56
commit a2dd1a1404
129 changed files with 7183 additions and 0 deletions

View file

@ -0,0 +1,40 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import i18n from "../../../i18n";
export interface AttributeBulkDeleteDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
quantity: string;
open: boolean;
onConfirm: () => void;
onClose: () => void;
}
const AttributeBulkDeleteDialog: React.StatelessComponent<
AttributeBulkDeleteDialogProps
> = ({ confirmButtonState, quantity, onClose, onConfirm, open }) => (
<ActionDialog
open={open}
confirmButtonState={confirmButtonState}
onClose={onClose}
onConfirm={onConfirm}
title={i18n.t("Remove attributes")}
variant="delete"
>
<DialogContentText
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to remove <strong>{{ quantity }}</strong> attributes?",
{
quantity
}
)
}}
/>
</ActionDialog>
);
AttributeBulkDeleteDialog.displayName = "AttributeBulkDeleteDialog";
export default AttributeBulkDeleteDialog;

View file

@ -0,0 +1,2 @@
export { default } from './AttributeBulkDeleteDialog';
export * from './AttributeBulkDeleteDialog';

View file

@ -0,0 +1,45 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import i18n from "@saleor/i18n";
export interface AttributeDeleteDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
onConfirm: () => void;
onClose: () => void;
name: string;
}
const AttributeDeleteDialog: React.FC<AttributeDeleteDialogProps> = ({
name,
confirmButtonState,
onClose,
onConfirm,
open
}) => (
<ActionDialog
open={open}
onClose={onClose}
confirmButtonState={confirmButtonState}
onConfirm={onConfirm}
variant="delete"
title={i18n.t("Remove attribute")}
>
<DialogContentText
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to remove <strong>{{ name }}</strong>?",
{
name
}
)
}}
/>
</ActionDialog>
);
AttributeDeleteDialog.displayName = "AttributeDeleteDialog";
export default AttributeDeleteDialog;

View file

@ -0,0 +1,2 @@
export { default } from './AttributeDeleteDialog';
export * from './AttributeDeleteDialog';

View file

@ -0,0 +1,98 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import React from "react";
import slugify from "slugify";
import CardTitle from "@saleor/components/CardTitle";
import ControlledSwitch from "@saleor/components/ControlledSwitch";
import FormSpacer from "@saleor/components/FormSpacer";
import SingleSelectField from "@saleor/components/SingleSelectField";
import i18n from "@saleor/i18n";
import { FormErrors } from "@saleor/types";
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
import { AttributePageFormData } from "../AttributePage";
export interface AttributeDetailsProps {
canChangeType: boolean;
data: AttributePageFormData;
disabled: boolean;
errors: FormErrors<"name" | "slug" | "inputType">;
onChange: (event: React.ChangeEvent<any>) => void;
}
const inputTypeChoices = [
{
label: i18n.t("Dropdown"),
value: AttributeInputTypeEnum.DROPDOWN
},
{
label: i18n.t("Multiple Select"),
value: AttributeInputTypeEnum.MULTISELECT
}
];
const AttributeDetails: React.FC<AttributeDetailsProps> = ({
canChangeType,
data,
disabled,
errors,
onChange
}) => (
<Card>
<CardTitle title={i18n.t("General Information")} />
<CardContent>
<TextField
disabled={disabled}
error={!!errors.name}
label={i18n.t("Default Label")}
name={"name" as keyof AttributePageFormData}
fullWidth
helperText={errors.name}
value={data.name}
onChange={onChange}
/>
<FormSpacer />
<TextField
disabled={disabled}
error={!!errors.slug}
label={i18n.t("Attribute Code")}
name={"slug" as keyof AttributePageFormData}
placeholder={slugify(data.name).toLowerCase()}
fullWidth
helperText={
errors.slug ||
i18n.t("This is used internally. Make sure you dont use spaces", {
context: "slug input"
})
}
value={data.slug}
onChange={onChange}
/>
<FormSpacer />
<SingleSelectField
choices={inputTypeChoices}
disabled={disabled || !canChangeType}
error={!!errors.inputType}
hint={errors.inputType}
label={i18n.t("Catalog Input type for Store Owner", {
context: "attribute input type"
})}
name="inputType"
onChange={onChange}
value={data.inputType}
/>
<FormSpacer />
<ControlledSwitch
checked={data.valueRequired}
label={i18n.t("Value Required", {
context: "attribute must have value"
})}
name={"valueRequired" as keyof AttributePageFormData}
onChange={onChange}
/>
</CardContent>
</Card>
);
AttributeDetails.displayName = "AttributeDetails";
export default AttributeDetails;

View file

@ -0,0 +1,2 @@
export { default } from './AttributeDetails';
export * from './AttributeDetails';

View file

@ -0,0 +1,183 @@
import { Theme } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import makeStyles from "@material-ui/styles/makeStyles";
import React from "react";
import Checkbox from "@saleor/components/Checkbox";
import Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import i18n from "@saleor/i18n";
import { renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "@saleor/types";
import { translateBoolean } from "@saleor/utils/i18n";
import { AttributeList_attributes_edges_node } from "../../types/AttributeList";
export interface AttributeListProps extends ListProps, ListActions {
attributes: AttributeList_attributes_edges_node[];
}
const useStyles = makeStyles((theme: Theme) => ({
[theme.breakpoints.up("lg")]: {
colFaceted: {
width: 150
},
colName: {
width: "auto"
},
colSearchable: {
width: 150
},
colSlug: {
width: 200
},
colVisible: {
width: 150
}
},
colFaceted: {
textAlign: "center"
},
colName: {},
colSearchable: {
textAlign: "center"
},
colSlug: {},
colVisible: {
textAlign: "center"
},
link: {
cursor: "pointer"
}
}));
const numberOfColumns = 6;
const AttributeList: React.StatelessComponent<AttributeListProps> = ({
attributes,
disabled,
isChecked,
onNextPage,
onPreviousPage,
onRowClick,
pageInfo,
selected,
toggle,
toggleAll,
toolbar
}) => {
const classes = useStyles({});
return (
<Table>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={attributes}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colSlug}>
{i18n.t("Attribute Code", { context: "attribute slug" })}
</TableCell>
<TableCell className={classes.colName}>
{i18n.t("Default Label", { context: "attribute name" })}
</TableCell>
<TableCell className={classes.colVisible}>
{i18n.t("Visible", { context: "attribute visibility" })}
</TableCell>
<TableCell className={classes.colSearchable}>
{i18n.t("Searchable", {
context: "attribute can be searched in dashboard"
})}
</TableCell>
<TableCell className={classes.colFaceted}>
{i18n.t("Use in faceted search", {
context: "attribute can be searched in storefront"
})}
</TableCell>
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
<TableBody>
{renderCollection(
attributes,
attribute => {
const isSelected = attribute ? isChecked(attribute.id) : false;
return (
<TableRow
selected={isSelected}
hover={!!attribute}
key={attribute ? attribute.id : "skeleton"}
onClick={attribute && onRowClick(attribute.id)}
className={classes.link}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(attribute.id)}
/>
</TableCell>
<TableCell className={classes.colSlug}>
{attribute ? attribute.slug : <Skeleton />}
</TableCell>
<TableCell className={classes.colName}>
{attribute ? attribute.name : <Skeleton />}
</TableCell>
<TableCell className={classes.colVisible}>
{attribute ? (
translateBoolean(attribute.visibleInStorefront)
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colSearchable}>
{attribute ? (
translateBoolean(attribute.filterableInDashboard)
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colFaceted}>
{attribute ? (
translateBoolean(attribute.filterableInStorefront)
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
{i18n.t("No attributes found")}
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
);
};
AttributeList.displayName = "AttributeList";
export default AttributeList;

View file

@ -0,0 +1,2 @@
export { default } from './AttributeList';
export * from './AttributeList';

View file

@ -0,0 +1,33 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import AddIcon from "@material-ui/icons/Add";
import React from "react";
import Container from "../../../components/Container";
import PageHeader from "../../../components/PageHeader";
import i18n from "../../../i18n";
import { ListActions, PageListProps } from "../../../types";
import { AttributeList_attributes_edges_node } from "../../types/AttributeList";
import AttributeList from "../AttributeList/AttributeList";
export interface AttributeListPageProps extends PageListProps, ListActions {
attributes: AttributeList_attributes_edges_node[];
}
const AttributeListPage: React.FC<AttributeListPageProps> = ({
onAdd,
...listProps
}) => (
<Container>
<PageHeader title={i18n.t("Attributes")}>
<Button onClick={onAdd} color="primary" variant="contained">
{i18n.t("Add attribute")} <AddIcon />
</Button>
</PageHeader>
<Card>
<AttributeList {...listProps} />
</Card>
</Container>
);
AttributeListPage.displayName = "AttributeListPage";
export default AttributeListPage;

View file

@ -0,0 +1,2 @@
export { default } from './AttributeListPage';
export * from './AttributeListPage';

View file

@ -0,0 +1,160 @@
import React from "react";
import slugify from "slugify";
import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import i18n from "@saleor/i18n";
import { maybe } from "@saleor/misc";
import { ReorderAction, UserError } from "@saleor/types";
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
import {
AttributeDetailsFragment,
AttributeDetailsFragment_values
} from "../../types/AttributeDetailsFragment";
import AttributeDetails from "../AttributeDetails";
import AttributeProperties from "../AttributeProperties";
import AttributeValues from "../AttributeValues";
export interface AttributePageProps {
attribute: AttributeDetailsFragment | null;
disabled: boolean;
errors: UserError[];
saveButtonBarState: ConfirmButtonTransitionState;
values: AttributeDetailsFragment_values[];
onBack: () => void;
onDelete: () => void;
onSubmit: (data: AttributePageFormData) => void;
onValueAdd: () => void;
onValueDelete: (id: string) => void;
onValueReorder: ReorderAction;
onValueUpdate: (id: string) => void;
}
export interface AttributePageFormData {
filterableInDashboard: boolean;
inputType: AttributeInputTypeEnum;
filterableInStorefront: boolean;
name: string;
slug: string;
storefrontSearchPosition: string;
valueRequired: boolean;
visibleInStorefront: boolean;
}
const AttributePage: React.FC<AttributePageProps> = ({
attribute,
disabled,
errors,
saveButtonBarState,
values,
onBack,
onDelete,
onSubmit,
onValueAdd,
onValueDelete,
onValueReorder,
onValueUpdate
}) => {
const initialForm: AttributePageFormData =
attribute === null
? {
filterableInDashboard: true,
filterableInStorefront: true,
inputType: AttributeInputTypeEnum.DROPDOWN,
name: "",
slug: "",
storefrontSearchPosition: "",
valueRequired: true,
visibleInStorefront: true
}
: {
filterableInDashboard: maybe(
() => attribute.filterableInDashboard,
true
),
filterableInStorefront: maybe(
() => attribute.filterableInStorefront,
true
),
inputType: maybe(
() => attribute.inputType,
AttributeInputTypeEnum.DROPDOWN
),
name: maybe(() => attribute.name, ""),
slug: maybe(() => attribute.slug, ""),
storefrontSearchPosition: maybe(
() => attribute.storefrontSearchPosition.toString(),
""
),
valueRequired: maybe(() => attribute.valueRequired, true),
visibleInStorefront: maybe(() => attribute.visibleInStorefront, true)
};
const handleSubmit = (data: AttributePageFormData) =>
onSubmit({
...data,
slug: data.slug || slugify(data.name).toLowerCase()
});
return (
<Form errors={errors} initial={initialForm} onSubmit={handleSubmit}>
{({ change, errors: formErrors, data, submit }) => (
<Container>
<AppHeader onBack={onBack}>{i18n.t("Attributes")}</AppHeader>
<PageHeader
title={
attribute === null
? i18n.t("Create New Attribute", {
context: "page title"
})
: maybe(() => attribute.name)
}
/>
<Grid>
<div>
<AttributeDetails
canChangeType={attribute === null}
data={data}
disabled={disabled}
errors={formErrors}
onChange={change}
/>
<CardSpacer />
<AttributeValues
disabled={disabled}
values={values}
onValueAdd={onValueAdd}
onValueDelete={onValueDelete}
onValueReorder={onValueReorder}
onValueUpdate={onValueUpdate}
/>
</div>
<div>
<AttributeProperties
data={data}
errors={formErrors}
disabled={disabled}
onChange={change}
/>
</div>
</Grid>
<SaveButtonBar
disabled={disabled}
state={saveButtonBarState}
onCancel={onBack}
onSave={submit}
onDelete={attribute === null ? undefined : onDelete}
/>
</Container>
)}
</Form>
);
};
AttributePage.displayName = "AttributePage";
export default AttributePage;

View file

@ -0,0 +1,2 @@
export { default } from './AttributePage';
export * from './AttributePage';

View file

@ -0,0 +1,112 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import React from "react";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import ControlledSwitch from "@saleor/components/ControlledSwitch";
import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import i18n from "@saleor/i18n";
import { FormErrors } from "@saleor/types";
import { AttributePageFormData } from "../AttributePage";
export interface AttributePropertiesProps {
data: AttributePageFormData;
disabled: boolean;
errors: FormErrors<"storefrontSearchPosition">;
onChange: (event: React.ChangeEvent<any>) => void;
}
const AttributeProperties: React.FC<AttributePropertiesProps> = ({
data,
errors,
disabled,
onChange
}) => (
<Card>
<CardTitle title={i18n.t("Properties")} />
<CardContent>
{/* <Typography variant="subtitle1">
{i18n.t("General Properties")}
</Typography>
<Hr />
<CardSpacer />
<ControlledSwitch
name={"" as keyof AttributePageFormData}
checked={false}
disabled={disabled}
label={
<>
<span>{i18n.t("Variant Attribute")}</span>
<Typography variant="caption">
{i18n.t(
"If enabled, you'll be able to use this attribute to create product variants"
)}
</Typography>
</>
}
onChange={onChange}
/> */}
<Typography variant="subtitle1">
{i18n.t("Storefront Properties")}
</Typography>
<Hr />
<ControlledSwitch
name={"filterableInStorefront" as keyof AttributePageFormData}
checked={data.filterableInStorefront}
disabled={disabled}
label={i18n.t("Use in faceted navigation")}
onChange={onChange}
/>
{data.filterableInStorefront && (
<TextField
disabled={disabled}
error={!!errors.storefrontSearchPosition}
fullWidth
helperText={errors.storefrontSearchPosition}
name={"storefrontSearchPosition" as keyof AttributePageFormData}
label={i18n.t("Position in faceted navigation")}
value={data.storefrontSearchPosition}
onChange={onChange}
/>
)}
<FormSpacer />
<ControlledSwitch
name={"visibleInStorefront" as keyof AttributePageFormData}
checked={data.visibleInStorefront}
disabled={disabled}
label={i18n.t("Visible on Product Page in Storefront", {
context: "attribute"
})}
onChange={onChange}
/>
<CardSpacer />
<Typography variant="subtitle1">
{i18n.t("Dashboard Properties")}
</Typography>
<Hr />
<CardSpacer />
<ControlledSwitch
name={"filterableInDashboard" as keyof AttributePageFormData}
checked={data.filterableInDashboard}
disabled={disabled}
label={i18n.t("Use in Filtering")}
secondLabel={
<Typography variant="caption">
{i18n.t(
"If enabled, youll be able to use this attribute to filter products in product list."
)}
</Typography>
}
onChange={onChange}
/>
</CardContent>
</Card>
);
AttributeProperties.displayName = "AttributeProperties";
export default AttributeProperties;

View file

@ -0,0 +1,2 @@
export { default } from './AttributeProperties';
export * from './AttributeProperties';

View file

@ -0,0 +1,52 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import i18n from "@saleor/i18n";
export interface AttributeValueDeleteDialogProps {
attributeName: string;
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
name: string;
useName?: boolean;
onConfirm: () => void;
onClose: () => void;
}
const AttributeValueDeleteDialog: React.FC<AttributeValueDeleteDialogProps> = ({
attributeName,
name,
confirmButtonState,
useName,
onClose,
onConfirm,
open
}) => (
<ActionDialog
open={open}
onClose={onClose}
confirmButtonState={confirmButtonState}
onConfirm={onConfirm}
variant="delete"
title={i18n.t("Remove attribute value")}
>
<DialogContentText>
{useName
? i18n.t(
'Are you sure you want to remove "{{ name }}" value? If you remove it you wont be able to assign it to any of the products with "{{ attributeName }}" attribute.',
{
attributeName,
name
}
)
: i18n.t('Are you sure you want to remove "{{ name }}" value?', {
name
})}
</DialogContentText>
</ActionDialog>
);
AttributeValueDeleteDialog.displayName = "AttributeValueDeleteDialog";
export default AttributeValueDeleteDialog;

View file

@ -0,0 +1,2 @@
export { default } from './AttributeValueDeleteDialog';
export * from './AttributeValueDeleteDialog';

View file

@ -0,0 +1,97 @@
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 React from "react";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form";
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
import i18n from "@saleor/i18n";
import { maybe } from "@saleor/misc";
import { UserError } from "@saleor/types";
import { AttributeDetails_attribute_values } from "../../types/AttributeDetails";
export interface AttributeValueEditDialogFormData {
name: string;
}
export interface AttributeValueEditDialogProps {
attributeValue: AttributeDetails_attribute_values | null;
confirmButtonState: ConfirmButtonTransitionState;
disabled: boolean;
errors: UserError[];
open: boolean;
onSubmit: (data: AttributeValueEditDialogFormData) => void;
onClose: () => void;
}
const AttributeValueEditDialog: React.StatelessComponent<
AttributeValueEditDialogProps
> = ({
attributeValue,
confirmButtonState,
disabled,
errors: apiErrors,
onClose,
onSubmit,
open
}) => {
const initialForm: AttributeValueEditDialogFormData = {
name: maybe(() => attributeValue.name, "")
};
const errors = useModalDialogErrors(apiErrors, open);
return (
<Dialog onClose={onClose} open={open} fullWidth maxWidth="sm">
<DialogTitle>
{attributeValue === null
? i18n.t("Add Value", {
context: "add attribute value"
})
: i18n.t("Edit Value", {
context: "edit attribute value"
})}
</DialogTitle>
<Form errors={errors} initial={initialForm} onSubmit={onSubmit}>
{({ change, data, errors: formErrors, submit }) => (
<>
<DialogContent>
<TextField
autoFocus
disabled={disabled}
error={!!formErrors.name}
fullWidth
helperText={formErrors.name}
name={"name" as keyof AttributeValueEditDialogFormData}
label={i18n.t("Name", {
context: "attribute name"
})}
value={data.name}
onChange={change}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
{i18n.t("Cancel", { context: "button" })}
</Button>
<ConfirmButton
transitionState={confirmButtonState}
color="primary"
variant="contained"
onClick={submit}
>
{i18n.t("Save")}
</ConfirmButton>
</DialogActions>
</>
)}
</Form>
</Dialog>
);
};
AttributeValueEditDialog.displayName = "AttributeValueEditDialog";
export default AttributeValueEditDialog;

View file

@ -0,0 +1,2 @@
export { default } from './AttributeValueEditDialog';
export * from './AttributeValueEditDialog';

View file

@ -0,0 +1,129 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import IconButton from "@material-ui/core/IconButton";
import { Theme } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import DeleteIcon from "@material-ui/icons/Delete";
import makeStyles from "@material-ui/styles/makeStyles";
import React from "react";
import CardTitle from "@saleor/components/CardTitle";
import Skeleton from "@saleor/components/Skeleton";
import {
SortableTableBody,
SortableTableRow
} from "@saleor/components/SortableTable";
import i18n from "@saleor/i18n";
import { maybe, renderCollection, stopPropagation } from "@saleor/misc";
import { ReorderAction } from "@saleor/types";
import { AttributeDetailsFragment_values } from "../../types/AttributeDetailsFragment";
export interface AttributeValuesProps {
disabled: boolean;
values: AttributeDetailsFragment_values[];
onValueAdd: () => void;
onValueDelete: (id: string) => void;
onValueReorder: ReorderAction;
onValueUpdate: (id: string) => void;
}
const useStyles = makeStyles((theme: Theme) => ({
columnAdmin: {
width: "50%"
},
columnDrag: {
width: 48 + theme.spacing.unit * 1.5
},
columnStore: {
width: "50%"
},
dragIcon: {
cursor: "grab"
},
iconCell: {
"&:last-child": {
paddingRight: theme.spacing.unit
},
width: 48 + theme.spacing.unit * 1.5
},
link: {
cursor: "pointer"
}
}));
const AttributeValues: React.FC<AttributeValuesProps> = ({
disabled,
onValueAdd,
onValueDelete,
onValueReorder,
onValueUpdate,
values
}) => {
const classes = useStyles({});
return (
<Card>
<CardTitle
title={i18n.t("Attribute Values")}
toolbar={
<Button color="primary" variant="text" onClick={onValueAdd}>
{i18n.t("Add value", { context: "button" })}
</Button>
}
/>
<Table>
<TableHead>
<TableRow>
<TableCell className={classes.columnDrag} />
<TableCell className={classes.columnAdmin}>
{i18n.t("Admin")}
</TableCell>
<TableCell className={classes.columnStore}>
{i18n.t("Default Store View")}
</TableCell>
<TableCell />
</TableRow>
</TableHead>
<SortableTableBody onSortEnd={onValueReorder}>
{renderCollection(
values,
(value, valueIndex) => (
<SortableTableRow
className={!!value ? classes.link : undefined}
hover={!!value}
onClick={!!value ? () => onValueUpdate(value.id) : undefined}
key={maybe(() => value.id)}
index={valueIndex || 0}
>
<TableCell className={classes.columnAdmin}>
{maybe(() => value.slug) ? value.slug : <Skeleton />}
</TableCell>
<TableCell className={classes.columnStore}>
{maybe(() => value.name) ? value.name : <Skeleton />}
</TableCell>
<TableCell className={classes.iconCell}>
<IconButton
disabled={disabled}
onClick={stopPropagation(() => onValueDelete(value.id))}
>
<DeleteIcon color="primary" />
</IconButton>
</TableCell>
</SortableTableRow>
),
() => (
<TableRow>
<TableCell colSpan={2}>{i18n.t("No values found")}</TableCell>
</TableRow>
)
)}
</SortableTableBody>
</Table>
</Card>
);
};
AttributeValues.displayName = "AttributeValues";
export default AttributeValues;

View file

@ -0,0 +1,2 @@
export { default } from './AttributeValues';
export * from './AttributeValues';

512
src/attributes/fixtures.ts Normal file
View file

@ -0,0 +1,512 @@
import {
AttributeInputTypeEnum,
AttributeValueType
} from "@saleor/types/globalTypes";
import { AttributeList_attributes_edges_node } from "./types/AttributeList";
export const attribute = {
__typename: "Attribute" as "Attribute",
filterableInDashboard: false,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZTo5",
inputType: AttributeInputTypeEnum.DROPDOWN,
name: "Author",
slug: "author",
storefrontSearchPosition: 2,
valueRequired: true,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI0",
name: "John Doe",
slug: "john-doe",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI1",
name: "Milionare Pirate",
slug: "milionare-pirate",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
};
export const attributes: AttributeList_attributes_edges_node[] = [
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: false,
id: "UHJvZHVjdEF0dHJpYnV0ZTo5",
name: "Author",
slug: "author",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI0",
name: "John Doe",
slug: "john-doe",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI1",
name: "Milionare Pirate",
slug: "milionare-pirate",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: false,
id: "UHJvZHVjdEF0dHJpYnV0ZTo2",
name: "Box Size",
slug: "box-size",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE1",
name: "100g",
slug: "100g",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE2",
name: "250g",
slug: "250g",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE3",
name: "500g",
slug: "500g",
sortOrder: 2,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE4",
name: "1kg",
slug: "1kg",
sortOrder: 3,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: false
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: false,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZToz",
name: "Brand",
slug: "brand",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjY=",
name: "Saleor",
slug: "saleor",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: false
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZTo4",
name: "Candy Box Size",
slug: "candy-box-size",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIx",
name: "100g",
slug: "100g",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIy",
name: "250g",
slug: "250g",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIz",
name: "500g",
slug: "500g",
sortOrder: 2,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: false
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZTo1",
name: "Coffee Genre",
slug: "coffee-genre",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEz",
name: "Arabica",
slug: "arabica",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE0",
name: "Robusta",
slug: "robusta",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: false,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZToy",
name: "Collar",
slug: "collar",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM=",
name: "Round",
slug: "round",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjQ=",
name: "V-Neck",
slug: "v-neck",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjU=",
name: "Polo",
slug: "polo",
sortOrder: 2,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: false,
filterableInStorefront: false,
id: "UHJvZHVjdEF0dHJpYnV0ZTox",
name: "Color",
slug: "color",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE=",
name: "Blue",
slug: "blue",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI=",
name: "White",
slug: "white",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: false,
id: "UHJvZHVjdEF0dHJpYnV0ZToxMg==",
name: "Cover",
slug: "cover",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMw",
name: "Soft",
slug: "soft",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMx",
name: "Hard",
slug: "hard",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMy",
name: "Middle soft",
slug: "middle-soft",
sortOrder: 2,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMz",
name: "Middle hard",
slug: "middle-hard",
sortOrder: 3,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM0",
name: "Middle",
slug: "middle",
sortOrder: 4,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM1",
name: "Very hard",
slug: "very-hard",
sortOrder: 5,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: false
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZTo3",
name: "Flavor",
slug: "flavor",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE5",
name: "Sour",
slug: "sour",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIw",
name: "Sweet",
slug: "sweet",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: false,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZToxMQ==",
name: "Language",
slug: "language",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI4",
name: "English",
slug: "english",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI5",
name: "Pirate",
slug: "pirate",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZToxMA==",
name: "Publisher",
slug: "publisher",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI2",
name: "Mirumee Press",
slug: "mirumee-press",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI3",
name: "Saleor Publishing",
slug: "saleor-publishing",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZTo0",
name: "Size",
slug: "size",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjc=",
name: "XS",
slug: "xs",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjg=",
name: "S",
slug: "s",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjk=",
name: "M",
slug: "m",
sortOrder: 2,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEw",
name: "L",
slug: "l",
sortOrder: 3,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEx",
name: "XL",
slug: "xl",
sortOrder: 4,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEy",
name: "XXL",
slug: "xxl",
sortOrder: 5,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
}
].map(edge => edge.node);

55
src/attributes/index.tsx Normal file
View file

@ -0,0 +1,55 @@
import { parse as parseQs } from "qs";
import React from "react";
import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { WindowTitle } from "../components/WindowTitle";
import i18n from "../i18n";
import {
attributeAddPath,
AttributeAddUrlQueryParams,
attributeListPath,
AttributeListUrlQueryParams,
attributePath,
AttributeUrlQueryParams
} from "./urls";
import AttributeCreateComponent from "./views/AttributeCreate";
import AttributeDetailsComponent from "./views/AttributeDetails";
import AttributeListComponent from "./views/AttributeList";
const AttributeList: React.FC<RouteComponentProps<{}>> = ({ location }) => {
const qs = parseQs(location.search.substr(1));
const params: AttributeListUrlQueryParams = qs;
return <AttributeListComponent params={params} />;
};
const AttributeCreate: React.FC<RouteComponentProps<{}>> = ({ location }) => {
const qs = parseQs(location.search.substr(1));
const params: AttributeAddUrlQueryParams = qs;
return <AttributeCreateComponent params={params} />;
};
const AttributeDetails: React.FC<RouteComponentProps<{ id: string }>> = ({
location,
match
}) => {
const qs = parseQs(location.search.substr(1));
const params: AttributeUrlQueryParams = qs;
return (
<AttributeDetailsComponent
id={decodeURIComponent(match.params.id)}
params={params}
/>
);
};
export const AttributeSection: React.FC = () => (
<>
<WindowTitle title={i18n.t("Attributes")} />
<Switch>
<Route exact path={attributeListPath} component={AttributeList} />
<Route exact path={attributeAddPath} component={AttributeCreate} />
<Route path={attributePath(":id")} component={AttributeDetails} />
</Switch>
</>
);
export default AttributeSection;

182
src/attributes/mutations.ts Normal file
View file

@ -0,0 +1,182 @@
import gql from "graphql-tag";
import { TypedMutation } from "@saleor/mutations";
import { attributeDetailsFragment } from "./queries";
import {
AttributeBulkDelete,
AttributeBulkDeleteVariables
} from "./types/AttributeBulkDelete";
import {
AttributeCreate,
AttributeCreateVariables
} from "./types/AttributeCreate";
import {
AttributeDelete,
AttributeDeleteVariables
} from "./types/AttributeDelete";
import {
AttributeUpdate,
AttributeUpdateVariables
} from "./types/AttributeUpdate";
import {
AttributeValueCreate,
AttributeValueCreateVariables
} from "./types/AttributeValueCreate";
import {
AttributeValueDelete,
AttributeValueDeleteVariables
} from "./types/AttributeValueDelete";
import {
AttributeValueReorder,
AttributeValueReorderVariables
} from "./types/AttributeValueReorder";
import {
AttributeValueUpdate,
AttributeValueUpdateVariables
} from "./types/AttributeValueUpdate";
const attributeBulkDelete = gql`
mutation AttributeBulkDelete($ids: [ID!]!) {
attributeBulkDelete(ids: $ids) {
errors {
field
message
}
}
}
`;
export const AttributeBulkDeleteMutation = TypedMutation<
AttributeBulkDelete,
AttributeBulkDeleteVariables
>(attributeBulkDelete);
const attributeDelete = gql`
mutation AttributeDelete($id: ID!) {
attributeDelete(id: $id) {
errors {
field
message
}
}
}
`;
export const AttributeDeleteMutation = TypedMutation<
AttributeDelete,
AttributeDeleteVariables
>(attributeDelete);
export const attributeUpdateMutation = gql`
${attributeDetailsFragment}
mutation AttributeUpdate($id: ID!, $input: AttributeUpdateInput!) {
attributeUpdate(id: $id, input: $input) {
errors {
field
message
}
attribute {
...AttributeDetailsFragment
}
}
}
`;
export const AttributeUpdateMutation = TypedMutation<
AttributeUpdate,
AttributeUpdateVariables
>(attributeUpdateMutation);
const attributeValueDelete = gql`
${attributeDetailsFragment}
mutation AttributeValueDelete($id: ID!) {
attributeValueDelete(id: $id) {
errors {
field
message
}
attribute {
...AttributeDetailsFragment
}
}
}
`;
export const AttributeValueDeleteMutation = TypedMutation<
AttributeValueDelete,
AttributeValueDeleteVariables
>(attributeValueDelete);
export const attributeValueUpdateMutation = gql`
${attributeDetailsFragment}
mutation AttributeValueUpdate($id: ID!, $input: AttributeValueCreateInput!) {
attributeValueUpdate(id: $id, input: $input) {
errors {
field
message
}
attribute {
...AttributeDetailsFragment
}
}
}
`;
export const AttributeValueUpdateMutation = TypedMutation<
AttributeValueUpdate,
AttributeValueUpdateVariables
>(attributeValueUpdateMutation);
export const attributeValueCreateMutation = gql`
${attributeDetailsFragment}
mutation AttributeValueCreate($id: ID!, $input: AttributeValueCreateInput!) {
attributeValueCreate(attribute: $id, input: $input) {
errors {
field
message
}
attribute {
...AttributeDetailsFragment
}
}
}
`;
export const AttributeValueCreateMutation = TypedMutation<
AttributeValueCreate,
AttributeValueCreateVariables
>(attributeValueCreateMutation);
export const attributeCreateMutation = gql`
${attributeDetailsFragment}
mutation AttributeCreate($input: AttributeCreateInput!) {
attributeCreate(input: $input) {
errors {
field
message
}
attribute {
...AttributeDetailsFragment
}
}
}
`;
export const AttributeCreateMutation = TypedMutation<
AttributeCreate,
AttributeCreateVariables
>(attributeCreateMutation);
const attributeValueReorderMutation = gql`
mutation AttributeValueReorder($id: ID!, $move: ReorderInput!) {
attributeReorderValues(attributeId: $id, moves: [$move]) {
errors {
field
message
}
attribute {
id
values {
id
}
}
}
}
`;
export const AttributeValueReorderMutation = TypedMutation<
AttributeValueReorder,
AttributeValueReorderVariables
>(attributeValueReorderMutation);

86
src/attributes/queries.ts Normal file
View file

@ -0,0 +1,86 @@
import gql from "graphql-tag";
import { pageInfoFragment, TypedQuery } from "../queries";
import {
AttributeDetails,
AttributeDetailsVariables
} from "./types/AttributeDetails";
import { AttributeList, AttributeListVariables } from "./types/AttributeList";
export const attributeFragment = gql`
fragment AttributeFragment on Attribute {
id
name
slug
visibleInStorefront
filterableInDashboard
filterableInStorefront
}
`;
export const attributeDetailsFragment = gql`
${attributeFragment}
fragment AttributeDetailsFragment on Attribute {
...AttributeFragment
inputType
storefrontSearchPosition
valueRequired
values {
id
name
slug
type
value
}
}
`;
const attributeDetails = gql`
${attributeDetailsFragment}
query AttributeDetails($id: ID!) {
attribute(id: $id) {
...AttributeDetailsFragment
}
}
`;
export const AttributeDetailsQuery = TypedQuery<
AttributeDetails,
AttributeDetailsVariables
>(attributeDetails);
const attributeList = gql`
${attributeFragment}
${pageInfoFragment}
query AttributeList(
$query: String
$inCategory: ID
$inCollection: ID
$before: String
$after: String
$first: Int
$last: Int
) {
attributes(
query: $query
inCategory: $inCategory
inCollection: $inCollection
before: $before
after: $after
first: $first
last: $last
) {
edges {
node {
...AttributeFragment
}
}
pageInfo {
...PageInfoFragment
}
}
}
`;
export const AttributeListQuery = TypedQuery<
AttributeList,
AttributeListVariables
>(attributeList);

View file

@ -0,0 +1,26 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL mutation operation: AttributeBulkDelete
// ====================================================
export interface AttributeBulkDelete_attributeBulkDelete_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeBulkDelete_attributeBulkDelete {
__typename: "AttributeBulkDelete";
errors: AttributeBulkDelete_attributeBulkDelete_errors[] | null;
}
export interface AttributeBulkDelete {
attributeBulkDelete: AttributeBulkDelete_attributeBulkDelete | null;
}
export interface AttributeBulkDeleteVariables {
ids: string[];
}

View file

@ -0,0 +1,52 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeCreateInput, AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeCreate
// ====================================================
export interface AttributeCreate_attributeCreate_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeCreate_attributeCreate_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeCreate_attributeCreate_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeCreate_attributeCreate_attribute_values | null)[] | null;
}
export interface AttributeCreate_attributeCreate {
__typename: "AttributeCreate";
errors: AttributeCreate_attributeCreate_errors[] | null;
attribute: AttributeCreate_attributeCreate_attribute | null;
}
export interface AttributeCreate {
attributeCreate: AttributeCreate_attributeCreate | null;
}
export interface AttributeCreateVariables {
input: AttributeCreateInput;
}

View file

@ -0,0 +1,26 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL mutation operation: AttributeDelete
// ====================================================
export interface AttributeDelete_attributeDelete_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeDelete_attributeDelete {
__typename: "AttributeDelete";
errors: AttributeDelete_attributeDelete_errors[] | null;
}
export interface AttributeDelete {
attributeDelete: AttributeDelete_attributeDelete | null;
}
export interface AttributeDeleteVariables {
id: string;
}

View file

@ -0,0 +1,40 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL query operation: AttributeDetails
// ====================================================
export interface AttributeDetails_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeDetails_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeDetails_attribute_values | null)[] | null;
}
export interface AttributeDetails {
attribute: AttributeDetails_attribute | null;
}
export interface AttributeDetailsVariables {
id: string;
}

View file

@ -0,0 +1,32 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: AttributeDetailsFragment
// ====================================================
export interface AttributeDetailsFragment_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeDetailsFragment {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeDetailsFragment_values | null)[] | null;
}

View file

@ -0,0 +1,17 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL fragment: AttributeFragment
// ====================================================
export interface AttributeFragment {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
}

View file

@ -0,0 +1,50 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: AttributeList
// ====================================================
export interface AttributeList_attributes_edges_node {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
}
export interface AttributeList_attributes_edges {
__typename: "AttributeCountableEdge";
node: AttributeList_attributes_edges_node;
}
export interface AttributeList_attributes_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface AttributeList_attributes {
__typename: "AttributeCountableConnection";
edges: AttributeList_attributes_edges[];
pageInfo: AttributeList_attributes_pageInfo;
}
export interface AttributeList {
attributes: AttributeList_attributes | null;
}
export interface AttributeListVariables {
query?: string | null;
inCategory?: string | null;
inCollection?: string | null;
before?: string | null;
after?: string | null;
first?: number | null;
last?: number | null;
}

View file

@ -0,0 +1,53 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeUpdateInput, AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeUpdate
// ====================================================
export interface AttributeUpdate_attributeUpdate_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeUpdate_attributeUpdate_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeUpdate_attributeUpdate_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeUpdate_attributeUpdate_attribute_values | null)[] | null;
}
export interface AttributeUpdate_attributeUpdate {
__typename: "AttributeUpdate";
errors: AttributeUpdate_attributeUpdate_errors[] | null;
attribute: AttributeUpdate_attributeUpdate_attribute | null;
}
export interface AttributeUpdate {
attributeUpdate: AttributeUpdate_attributeUpdate | null;
}
export interface AttributeUpdateVariables {
id: string;
input: AttributeUpdateInput;
}

View file

@ -0,0 +1,53 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeValueCreateInput, AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeValueCreate
// ====================================================
export interface AttributeValueCreate_attributeValueCreate_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeValueCreate_attributeValueCreate_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeValueCreate_attributeValueCreate_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeValueCreate_attributeValueCreate_attribute_values | null)[] | null;
}
export interface AttributeValueCreate_attributeValueCreate {
__typename: "AttributeValueCreate";
errors: AttributeValueCreate_attributeValueCreate_errors[] | null;
attribute: AttributeValueCreate_attributeValueCreate_attribute | null;
}
export interface AttributeValueCreate {
attributeValueCreate: AttributeValueCreate_attributeValueCreate | null;
}
export interface AttributeValueCreateVariables {
id: string;
input: AttributeValueCreateInput;
}

View file

@ -0,0 +1,52 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeValueDelete
// ====================================================
export interface AttributeValueDelete_attributeValueDelete_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeValueDelete_attributeValueDelete_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeValueDelete_attributeValueDelete_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeValueDelete_attributeValueDelete_attribute_values | null)[] | null;
}
export interface AttributeValueDelete_attributeValueDelete {
__typename: "AttributeValueDelete";
errors: AttributeValueDelete_attributeValueDelete_errors[] | null;
attribute: AttributeValueDelete_attributeValueDelete_attribute | null;
}
export interface AttributeValueDelete {
attributeValueDelete: AttributeValueDelete_attributeValueDelete | null;
}
export interface AttributeValueDeleteVariables {
id: string;
}

View file

@ -0,0 +1,41 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ReorderInput } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeValueReorder
// ====================================================
export interface AttributeValueReorder_attributeReorderValues_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeValueReorder_attributeReorderValues_attribute_values {
__typename: "AttributeValue";
id: string;
}
export interface AttributeValueReorder_attributeReorderValues_attribute {
__typename: "Attribute";
id: string;
values: (AttributeValueReorder_attributeReorderValues_attribute_values | null)[] | null;
}
export interface AttributeValueReorder_attributeReorderValues {
__typename: "AttributeReorderValues";
errors: AttributeValueReorder_attributeReorderValues_errors[] | null;
attribute: AttributeValueReorder_attributeReorderValues_attribute | null;
}
export interface AttributeValueReorder {
attributeReorderValues: AttributeValueReorder_attributeReorderValues | null;
}
export interface AttributeValueReorderVariables {
id: string;
move: ReorderInput;
}

View file

@ -0,0 +1,53 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeValueCreateInput, AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeValueUpdate
// ====================================================
export interface AttributeValueUpdate_attributeValueUpdate_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeValueUpdate_attributeValueUpdate_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeValueUpdate_attributeValueUpdate_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeValueUpdate_attributeValueUpdate_attribute_values | null)[] | null;
}
export interface AttributeValueUpdate_attributeValueUpdate {
__typename: "AttributeValueUpdate";
errors: AttributeValueUpdate_attributeValueUpdate_errors[] | null;
attribute: AttributeValueUpdate_attributeValueUpdate_attribute | null;
}
export interface AttributeValueUpdate {
attributeValueUpdate: AttributeValueUpdate_attributeValueUpdate | null;
}
export interface AttributeValueUpdateVariables {
id: string;
input: AttributeValueCreateInput;
}

38
src/attributes/urls.ts Normal file
View file

@ -0,0 +1,38 @@
import { stringify as stringifyQs } from "qs";
import urlJoin from "url-join";
import { BulkAction, Dialog, Pagination, SingleAction } from "../types";
export const attributeSection = "/attributes/";
export type AttributeListUrlDialog = "remove";
export type AttributeListUrlQueryParams = BulkAction &
Dialog<AttributeListUrlDialog> &
Pagination;
export const attributeListPath = attributeSection;
export const attributeListUrl = (params?: AttributeListUrlQueryParams) =>
attributeListPath + "?" + stringifyQs(params);
export type AttributeAddUrlDialog =
| "add-value"
| "edit-value"
| "remove-value"
| "remove-values";
export type AttributeAddUrlQueryParams = Dialog<AttributeAddUrlDialog> &
SingleAction;
export const attributeAddPath = urlJoin(attributeSection, "add");
export const attributeAddUrl = (params?: AttributeAddUrlQueryParams) =>
attributeAddPath + "?" + stringifyQs(params);
export type AttributeUrlDialog =
| "add-value"
| "edit-value"
| "remove"
| "remove-value"
| "remove-values";
export type AttributeUrlQueryParams = BulkAction &
Dialog<AttributeUrlDialog> &
SingleAction;
export const attributePath = (id: string) => urlJoin(attributeSection, id);
export const attributeUrl = (id: string, params?: AttributeUrlQueryParams) =>
attributePath(encodeURIComponent(id)) + "?" + stringifyQs(params);

View file

@ -0,0 +1,206 @@
import React from "react";
import slugify from "slugify";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import i18n from "@saleor/i18n";
import { getMutationState, maybe } from "@saleor/misc";
import { ReorderEvent, UserError } from "@saleor/types";
import {
add,
isSelected,
move,
remove,
updateAtIndex
} from "@saleor/utils/lists";
import AttributePage from "../../components/AttributePage";
import AttributeValueDeleteDialog from "../../components/AttributeValueDeleteDialog";
import AttributeValueEditDialog, {
AttributeValueEditDialogFormData
} from "../../components/AttributeValueEditDialog";
import { AttributeCreateMutation } from "../../mutations";
import { AttributeCreate } from "../../types/AttributeCreate";
import {
attributeAddUrl,
AttributeAddUrlDialog,
AttributeAddUrlQueryParams,
attributeListUrl,
attributeUrl
} from "../../urls";
interface AttributeDetailsProps {
params: AttributeAddUrlQueryParams;
}
function areValuesEqual(
a: AttributeValueEditDialogFormData,
b: AttributeValueEditDialogFormData
) {
return a.name === b.name;
}
const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
const navigate = useNavigator();
const notify = useNotifier();
const [values, setValues] = React.useState<
AttributeValueEditDialogFormData[]
>([]);
const [valueErrors, setValueErrors] = React.useState<UserError[]>([]);
const id = params.id ? parseInt(params.id, 0) : undefined;
const closeModal = () =>
navigate(
attributeAddUrl({
...params,
action: undefined,
id: undefined
}),
true
);
const openModal = (action: AttributeAddUrlDialog, valueId?: string) =>
navigate(
attributeAddUrl({
...params,
action,
id: valueId
})
);
const handleValueDelete = () => {
setValues(remove(values[params.id], values, areValuesEqual));
closeModal();
};
const handleCreate = (data: AttributeCreate) => {
if (data.attributeCreate.errors.length === 0) {
notify({ text: i18n.t("Successfully created attribute") });
navigate(attributeUrl(data.attributeCreate.attribute.id));
}
};
const handleValueUpdate = (input: AttributeValueEditDialogFormData) => {
if (isSelected(input, values, areValuesEqual)) {
setValueErrors([
{
field: "name",
message: i18n.t("A value named {{ name }} already exists", {
context: "value edit error",
name: input.name
})
}
]);
} else {
setValues(updateAtIndex(input, values, id));
closeModal();
}
};
const handleValueCreate = (input: AttributeValueEditDialogFormData) => {
if (isSelected(input, values, areValuesEqual)) {
setValueErrors([
{
field: "name",
message: i18n.t("A value named {{ name }} already exists", {
context: "value edit error",
name: input.name
})
}
]);
} else {
setValues(add(input, values));
closeModal();
}
};
const handleValueReorder = ({ newIndex, oldIndex }: ReorderEvent) =>
setValues(move(values[oldIndex], values, areValuesEqual, newIndex));
return (
<AttributeCreateMutation onCompleted={handleCreate}>
{(attributeCreate, attributeCreateOpts) => {
const createTransitionState = getMutationState(
attributeCreateOpts.called,
attributeCreateOpts.loading,
maybe(() => attributeCreateOpts.data.attributeCreate.errors)
);
return (
<>
<AttributePage
attribute={null}
disabled={false}
errors={maybe(
() => attributeCreateOpts.data.attributeCreate.errors,
[]
)}
onBack={() => navigate(attributeListUrl())}
onDelete={undefined}
onSubmit={input =>
attributeCreate({
variables: {
input: {
...input,
storefrontSearchPosition: parseInt(
input.storefrontSearchPosition,
0
),
values: values.map(value => ({
name: value.name
}))
}
}
})
}
onValueAdd={() => openModal("add-value")}
onValueDelete={id => openModal("remove-value", id)}
onValueReorder={handleValueReorder}
onValueUpdate={id => openModal("edit-value", id)}
saveButtonBarState={createTransitionState}
values={values.map((value, valueIndex) => ({
__typename: "AttributeValue" as "AttributeValue",
id: valueIndex.toString(),
slug: slugify(value.name).toLowerCase(),
sortOrder: valueIndex,
type: null,
value: null,
...value
}))}
/>
<AttributeValueEditDialog
attributeValue={null}
confirmButtonState="default"
disabled={false}
errors={valueErrors}
open={params.action === "add-value"}
onClose={closeModal}
onSubmit={handleValueCreate}
/>
{values.length > 0 && (
<>
<AttributeValueDeleteDialog
attributeName={undefined}
open={params.action === "remove-value"}
name={maybe(() => values[id].name, "...")}
confirmButtonState="default"
onClose={closeModal}
onConfirm={handleValueDelete}
/>
<AttributeValueEditDialog
attributeValue={maybe(() => values[params.id])}
confirmButtonState="default"
disabled={false}
errors={valueErrors}
open={params.action === "edit-value"}
onClose={closeModal}
onSubmit={handleValueUpdate}
/>
</>
)}
</>
);
}}
</AttributeCreateMutation>
);
};
AttributeDetails.displayName = "AttributeDetails";
export default AttributeDetails;

View file

@ -0,0 +1,2 @@
export * from "./AttributeCreate";
export { default } from "./AttributeCreate";

View file

@ -0,0 +1,360 @@
import React from "react";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import i18n from "@saleor/i18n";
import { getMutationState, maybe } from "@saleor/misc";
import { ReorderEvent } from "@saleor/types";
import { move } from "@saleor/utils/lists";
import AttributeDeleteDialog from "../../components/AttributeDeleteDialog";
import AttributePage from "../../components/AttributePage";
import AttributeValueDeleteDialog from "../../components/AttributeValueDeleteDialog";
import AttributeValueEditDialog from "../../components/AttributeValueEditDialog";
import {
AttributeDeleteMutation,
AttributeUpdateMutation,
AttributeValueCreateMutation,
AttributeValueDeleteMutation,
AttributeValueReorderMutation,
AttributeValueUpdateMutation
} from "../../mutations";
import { AttributeDetailsQuery } from "../../queries";
import { AttributeDelete } from "../../types/AttributeDelete";
import { AttributeUpdate } from "../../types/AttributeUpdate";
import { AttributeValueCreate } from "../../types/AttributeValueCreate";
import { AttributeValueDelete } from "../../types/AttributeValueDelete";
import { AttributeValueReorder } from "../../types/AttributeValueReorder";
import { AttributeValueUpdate } from "../../types/AttributeValueUpdate";
import {
attributeListUrl,
attributeUrl,
AttributeUrlDialog,
AttributeUrlQueryParams
} from "../../urls";
interface AttributeDetailsProps {
id: string;
params: AttributeUrlQueryParams;
}
const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
const navigate = useNavigator();
const notify = useNotifier();
const closeModal = () =>
navigate(
attributeUrl(id, {
...params,
action: undefined,
id: undefined,
ids: undefined
}),
true
);
const openModal = (action: AttributeUrlDialog, valueId?: string) =>
navigate(
attributeUrl(id, {
...params,
action,
id: valueId
})
);
const handleDelete = (data: AttributeDelete) => {
if (data.attributeDelete.errors.length === 0) {
notify({ text: i18n.t("Attribute removed") });
navigate(attributeListUrl());
}
};
const handleValueDelete = (data: AttributeValueDelete) => {
if (data.attributeValueDelete.errors.length === 0) {
notify({ text: i18n.t("Value removed") });
closeModal();
}
};
const handleUpdate = (data: AttributeUpdate) => {
if (data.attributeUpdate.errors.length === 0) {
notify({ text: i18n.t("Saved changes") });
}
};
const handleValueUpdate = (data: AttributeValueUpdate) => {
if (data.attributeValueUpdate.errors.length === 0) {
notify({ text: i18n.t("Saved changes") });
closeModal();
}
};
const handleValueCreate = (data: AttributeValueCreate) => {
if (data.attributeValueCreate.errors.length === 0) {
notify({ text: i18n.t("Added new value") });
closeModal();
}
};
const handleValueReorderMutation = (data: AttributeValueReorder) => {
if (data.attributeReorderValues.errors.length !== 0) {
notify({
text: i18n.t("Error: {{ errorMessage }}", {
errorMessage: data.attributeReorderValues.errors[0].message
})
});
}
};
return (
<AttributeDetailsQuery variables={{ id }}>
{({ data, loading }) => (
<AttributeDeleteMutation onCompleted={handleDelete}>
{(attributeDelete, attributeDeleteOpts) => (
<AttributeValueDeleteMutation onCompleted={handleValueDelete}>
{(attributeValueDelete, attributeValueDeleteOpts) => (
<AttributeUpdateMutation onCompleted={handleUpdate}>
{(attributeUpdate, attributeUpdateOpts) => (
<AttributeValueUpdateMutation
onCompleted={handleValueUpdate}
>
{(attributeValueUpdate, attributeValueUpdateOpts) => (
<AttributeValueCreateMutation
onCompleted={handleValueCreate}
>
{(attributeValueCreate, attributeValueCreateOpts) => (
<AttributeValueReorderMutation
onCompleted={handleValueReorderMutation}
>
{attributeValueReorder => {
const deleteTransitionState = getMutationState(
attributeDeleteOpts.called,
attributeDeleteOpts.loading,
maybe(
() =>
attributeDeleteOpts.data.attributeDelete
.errors
)
);
const deleteValueTransitionState = getMutationState(
attributeValueDeleteOpts.called,
attributeValueDeleteOpts.loading,
maybe(
() =>
attributeValueDeleteOpts.data
.attributeValueDelete.errors
)
);
const updateTransitionState = getMutationState(
attributeUpdateOpts.called,
attributeUpdateOpts.loading,
maybe(
() =>
attributeUpdateOpts.data.attributeUpdate
.errors
)
);
const updateValueTransitionState = getMutationState(
attributeValueUpdateOpts.called,
attributeValueUpdateOpts.loading,
maybe(
() =>
attributeValueUpdateOpts.data
.attributeValueUpdate.errors
)
);
const createValueTransitionState = getMutationState(
attributeValueCreateOpts.called,
attributeValueCreateOpts.loading,
maybe(
() =>
attributeValueCreateOpts.data
.attributeValueCreate.errors
)
);
const handleValueReorder = ({
newIndex,
oldIndex
}: ReorderEvent) =>
attributeValueReorder({
optimisticResponse: {
attributeReorderValues: {
__typename: "AttributeReorderValues",
attribute: {
...data.attribute,
values: move(
data.attribute.values[oldIndex],
data.attribute.values,
(a, b) => a.id === b.id,
newIndex
)
},
errors: []
}
},
variables: {
id,
move: {
id: data.attribute.values[oldIndex].id,
sortOrder: newIndex - oldIndex
}
}
});
return (
<>
<AttributePage
attribute={maybe(() => data.attribute)}
disabled={loading}
errors={maybe(
() =>
attributeUpdateOpts.data
.attributeUpdate.errors,
[]
)}
onBack={() =>
navigate(attributeListUrl())
}
onDelete={() => openModal("remove")}
onSubmit={data => {
const input = {
...data,
inputType: undefined
};
attributeUpdate({
variables: {
id,
input: {
...input,
storefrontSearchPosition: parseInt(
input.storefrontSearchPosition,
0
)
}
}
});
}}
onValueAdd={() => openModal("add-value")}
onValueDelete={id =>
openModal("remove-value", id)
}
onValueReorder={handleValueReorder}
onValueUpdate={id =>
openModal("edit-value", id)
}
saveButtonBarState={updateTransitionState}
values={maybe(
() => data.attribute.values
)}
/>
<AttributeDeleteDialog
open={params.action === "remove"}
name={maybe(
() => data.attribute.name,
"..."
)}
confirmButtonState={deleteTransitionState}
onClose={closeModal}
onConfirm={() =>
attributeDelete({
variables: {
id
}
})
}
/>
<AttributeValueDeleteDialog
attributeName={maybe(
() => data.attribute.name,
"..."
)}
open={params.action === "remove-value"}
name={maybe(
() =>
data.attribute.values.find(
value => params.id === value.id
).name,
"..."
)}
useName={true}
confirmButtonState={
deleteValueTransitionState
}
onClose={closeModal}
onConfirm={() =>
attributeValueDelete({
variables: {
id: params.id
}
})
}
/>
<AttributeValueEditDialog
attributeValue={null}
confirmButtonState={
createValueTransitionState
}
disabled={loading}
errors={maybe(
() =>
attributeValueCreateOpts.data
.attributeValueCreate.errors,
[]
)}
open={params.action === "add-value"}
onClose={closeModal}
onSubmit={input =>
attributeValueCreate({
variables: {
id,
input
}
})
}
/>
<AttributeValueEditDialog
attributeValue={maybe(() =>
data.attribute.values.find(
value => params.id === value.id
)
)}
confirmButtonState={
updateValueTransitionState
}
disabled={loading}
errors={maybe(
() =>
attributeValueUpdateOpts.data
.attributeValueUpdate.errors,
[]
)}
open={params.action === "edit-value"}
onClose={closeModal}
onSubmit={input =>
attributeValueUpdate({
variables: {
id: data.attribute.values.find(
value => params.id === value.id
).id,
input
}
})
}
/>
</>
);
}}
</AttributeValueReorderMutation>
)}
</AttributeValueCreateMutation>
)}
</AttributeValueUpdateMutation>
)}
</AttributeUpdateMutation>
)}
</AttributeValueDeleteMutation>
)}
</AttributeDeleteMutation>
)}
</AttributeDetailsQuery>
);
};
AttributeDetails.displayName = "AttributeDetails";
export default AttributeDetails;

View file

@ -0,0 +1,2 @@
export * from "./AttributeDetails";
export { default } from "./AttributeDetails";

View file

@ -0,0 +1,137 @@
import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete";
import React from "react";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import usePaginator, {
createPaginationState
} from "@saleor/hooks/usePaginator";
import { PAGINATE_BY } from "../../../config";
import useBulkActions from "../../../hooks/useBulkActions";
import i18n from "../../../i18n";
import { getMutationState, maybe } from "../../../misc";
import AttributeBulkDeleteDialog from "../../components/AttributeBulkDeleteDialog";
import AttributeListPage from "../../components/AttributeListPage";
import { AttributeBulkDeleteMutation } from "../../mutations";
import { AttributeListQuery } from "../../queries";
import { AttributeBulkDelete } from "../../types/AttributeBulkDelete";
import {
attributeAddUrl,
attributeListUrl,
AttributeListUrlDialog,
AttributeListUrlQueryParams,
attributeUrl
} from "../../urls";
interface AttributeListProps {
params: AttributeListUrlQueryParams;
}
const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
const navigate = useNavigator();
const paginate = usePaginator();
const notify = useNotifier();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids
);
const closeModal = () =>
navigate(
attributeListUrl({
...params,
action: undefined,
ids: undefined
}),
true
);
const openModal = (action: AttributeListUrlDialog, ids?: string[]) =>
navigate(
attributeListUrl({
...params,
action,
ids
})
);
const paginationState = createPaginationState(PAGINATE_BY, params);
const queryVariables = React.useMemo(() => paginationState, [params]);
return (
<AttributeListQuery variables={queryVariables}>
{({ data, loading, refetch }) => {
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
maybe(() => data.attributes.pageInfo),
paginationState,
params
);
const handleBulkDelete = (data: AttributeBulkDelete) => {
if (data.attributeBulkDelete.errors.length === 0) {
closeModal();
notify({
text: i18n.t("Attributes removed")
});
reset();
refetch();
}
};
return (
<AttributeBulkDeleteMutation onCompleted={handleBulkDelete}>
{(attributeBulkDelete, attributeBulkDeleteOpts) => {
const bulkDeleteMutationState = getMutationState(
attributeBulkDeleteOpts.called,
attributeBulkDeleteOpts.loading,
maybe(
() => attributeBulkDeleteOpts.data.attributeBulkDelete.errors
)
);
return (
<>
<AttributeListPage
attributes={maybe(() =>
data.attributes.edges.map(edge => edge.node)
)}
disabled={loading || attributeBulkDeleteOpts.loading}
isChecked={isSelected}
onAdd={() => navigate(attributeAddUrl())}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
onRowClick={id => () => navigate(attributeUrl(id))}
pageInfo={pageInfo}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
toolbar={
<IconButton
color="primary"
onClick={() => openModal("remove", listElements)}
>
<DeleteIcon />
</IconButton>
}
/>
<AttributeBulkDeleteDialog
confirmButtonState={bulkDeleteMutationState}
open={params.action === "remove"}
onConfirm={() =>
attributeBulkDelete({ variables: { ids: params.ids } })
}
onClose={closeModal}
quantity={maybe(() => params.ids.length.toString(), "...")}
/>
</>
);
}}
</AttributeBulkDeleteMutation>
);
}}
</AttributeListQuery>
);
};
AttributeList.displayName = "AttributeList";
export default AttributeList;

View file

@ -0,0 +1,2 @@
export * from "./AttributeList";
export { default } from "./AttributeList";

View file

@ -0,0 +1,103 @@
import ClickAwayListener from "@material-ui/core/ClickAwayListener";
import Grow from "@material-ui/core/Grow";
import Popper from "@material-ui/core/Popper";
import { Theme } from "@material-ui/core/styles";
import { fade } from "@material-ui/core/styles/colorManipulator";
import makeStyles from "@material-ui/styles/makeStyles";
import React from "react";
import ColumnPickerButton from "./ColumnPickerButton";
import ColumnPickerContent, {
ColumnPickerContentProps
} from "./ColumnPickerContent";
export interface ColumnPickerProps extends ColumnPickerContentProps {
className?: string;
initial?: boolean;
}
const useStyles = makeStyles(
(theme: Theme) => ({
popper: {
boxShadow: `0px 5px 10px 0 ${fade(theme.palette.common.black, 0.05)}`,
marginTop: theme.spacing.unit * 2,
zIndex: 1
}
}),
{
name: "ColumnPicker"
}
);
const ColumnPicker: React.FC<ColumnPickerProps> = props => {
const {
className,
columns,
initial = false,
selectedColumns,
onCancel,
onColumnToggle,
onReset,
onSave
} = props;
const classes = useStyles(props);
const anchor = React.useRef<HTMLDivElement>();
const [isExpanded, setExpansionState] = React.useState(false);
React.useEffect(() => {
setTimeout(() => setExpansionState(initial), 100);
}, []);
const handleCancel = React.useCallback(() => {
setExpansionState(false);
onCancel();
}, []);
const handleSave = () => {
setExpansionState(false);
onSave();
};
return (
<div ref={anchor} className={className}>
<ColumnPickerButton
active={isExpanded}
onClick={() => setExpansionState(prevState => !prevState)}
/>
<Popper
className={classes.popper}
open={isExpanded}
anchorEl={anchor.current}
transition
disablePortal
placement="bottom-end"
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom" ? "right bottom" : "right top"
}}
>
<ClickAwayListener
onClickAway={() => setExpansionState(false)}
mouseEvent="onClick"
>
<ColumnPickerContent
columns={columns}
selectedColumns={selectedColumns}
onCancel={handleCancel}
onColumnToggle={onColumnToggle}
onReset={onReset}
onSave={handleSave}
/>
</ClickAwayListener>
</Grow>
)}
</Popper>
</div>
);
};
export default ColumnPicker;

View file

@ -0,0 +1,67 @@
import Button from "@material-ui/core/Button";
import { Theme } from "@material-ui/core/styles";
import { fade } from "@material-ui/core/styles/colorManipulator";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import makeStyles from "@material-ui/styles/makeStyles";
import classNames from "classnames";
import React from "react";
import i18n from "@saleor/i18n";
interface ColumnPickerButtonProps {
active: boolean;
className?: string;
onClick: () => void;
}
const useStyles = makeStyles(
(theme: Theme) => ({
icon: {
marginLeft: theme.spacing.unit * 2,
transition: theme.transitions.duration.short + "ms"
},
root: {
"& span": {
color: theme.palette.primary.main
},
paddingRight: theme.spacing.unit
},
rootActive: {
background: fade(theme.palette.primary.main, 0.1)
},
rotate: {
transform: "rotate(180deg)"
}
}),
{
name: "ColumnPickerButton"
}
);
const ColumnPickerButton: React.FC<ColumnPickerButtonProps> = props => {
const { active, className, onClick } = props;
const classes = useStyles(props);
return (
<Button
className={classNames(classes.root, className, {
[classes.rootActive]: active
})}
color="primary"
onClick={onClick}
variant="outlined"
>
{i18n.t("Columns", {
context: "select visible columns button"
})}
<ArrowDropDownIcon
color="primary"
className={classNames(classes.icon, {
[classes.rotate]: active
})}
/>
</Button>
);
};
export default ColumnPickerButton;

View file

@ -0,0 +1,128 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import { Theme } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import makeStyles from "@material-ui/styles/makeStyles";
import classNames from "classnames";
import React from "react";
import useElementScroll from "@saleor/hooks/useElementScroll";
import i18n from "@saleor/i18n";
import { isSelected } from "@saleor/utils/lists";
import ControlledCheckbox from "../ControlledCheckbox";
import Hr from "../Hr";
export interface ColumnPickerChoice {
label: string;
value: string;
}
export interface ColumnPickerContentProps {
columns: ColumnPickerChoice[];
selectedColumns: string[];
onCancel: () => void;
onColumnToggle: (column: string) => void;
onReset: () => void;
onSave: () => void;
}
const useStyles = makeStyles((theme: Theme) => ({
actionBar: {
display: "flex",
justifyContent: "space-between"
},
actionBarContainer: {
boxShadow: `0px 0px 0px 0px ${theme.palette.background.paper}`,
transition: theme.transitions.duration.short + "ms"
},
content: {
display: "grid",
gridColumnGap: theme.spacing.unit * 3,
gridTemplateColumns: "repeat(3, 1fr)",
maxHeight: 256,
overflowX: "visible",
overflowY: "scroll",
padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px`
},
contentContainer: {
padding: 0
},
dropShadow: {
boxShadow: `0px -5px 10px 0px ${theme.overrides.MuiCard.root.borderColor}`
}
}));
const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => {
const {
columns,
selectedColumns,
onCancel,
onColumnToggle,
onReset,
onSave
} = props;
const classes = useStyles(props);
const anchor = React.useRef<HTMLDivElement>();
const scrollPosition = useElementScroll(anchor);
const dropShadow = anchor.current
? scrollPosition.y + anchor.current.clientHeight <
anchor.current.scrollHeight
: false;
return (
<Card>
<CardContent>
<Typography color="textSecondary">
{i18n.t(
"{{ numberOfSelected }} columns selected out of {{ numberOfTotal }}",
{
context: "pick columns to display",
numberOfSelected: selectedColumns.length,
numberOfTotal: columns.length
}
)}
</Typography>
</CardContent>
<Hr />
<CardContent className={classes.contentContainer}>
<div className={classes.content} ref={anchor}>
{columns.map(column => (
<ControlledCheckbox
checked={isSelected(
column.value,
selectedColumns,
(a, b) => a === b
)}
name={column.value}
label={column.label}
onChange={() => onColumnToggle(column.value)}
/>
))}
</div>
</CardContent>
<Hr />
<CardContent
className={classNames(classes.actionBarContainer, {
[classes.dropShadow]: dropShadow
})}
>
<div className={classes.actionBar}>
<Button color="default" onClick={onReset}>
{i18n.t("Reset")}
</Button>
<div>
<Button color="default" onClick={onCancel}>
{i18n.t("Cancel")}
</Button>
<Button color="primary" variant="contained" onClick={onSave}>
{i18n.t("Save")}
</Button>
</div>
</div>
</CardContent>
</Card>
);
};
export default ColumnPickerContent;

View file

@ -0,0 +1,4 @@
export { default } from "./ColumnPicker";
export * from "./ColumnPicker";
export * from "./ColumnPickerButton";
export * from "./ColumnPickerContent";

View file

@ -0,0 +1,95 @@
import FormControl from "@material-ui/core/FormControl";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormLabel from "@material-ui/core/FormLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles";
import classNames from "classnames";
import React from "react";
import i18n from "../../i18n";
const styles = createStyles({
formControl: {
padding: "0 15px",
width: "100%"
},
formLabel: {
marginLeft: "-5px",
paddingBottom: "10px"
},
radioLabel: {
"& > span": {
padding: "6px"
}
}
});
interface RadioGroupFieldProps extends WithStyles<typeof styles> {
choices: Array<{
value: string;
label: string | React.ReactNode;
}>;
className?: string;
disabled?: boolean;
error?: boolean;
hint?: string;
label?: string;
name?: string;
value?: string;
onChange: (event: React.ChangeEvent<any>) => void;
}
export const RadioGroupField = withStyles(styles, {
name: "RadioGroupField"
})(
({
className,
classes,
disabled,
error,
label,
choices,
value,
onChange,
name,
hint
}: RadioGroupFieldProps) => {
return (
<FormControl
className={classNames(classes.formControl, className)}
error={error}
disabled={disabled}
>
{label ? (
<FormLabel className={classes.formLabel}>{label}</FormLabel>
) : null}
<RadioGroup
aria-label={name}
name={name}
value={value}
onChange={onChange}
>
{choices.length > 0 ? (
choices.map(choice => (
<FormControlLabel
value={choice.value}
className={classes.radioLabel}
control={<Radio color="primary" />}
label={choice.label}
key={choice.value}
/>
))
) : (
<MenuItem disabled={true}>{i18n.t("No results found")}</MenuItem>
)}
</RadioGroup>
{hint && <FormHelperText>{hint}</FormHelperText>}
</FormControl>
);
}
);
RadioGroupField.displayName = "RadioGroupField";
export default RadioGroupField;

View file

@ -0,0 +1,2 @@
export { default } from "./RadioGroupField";
export * from "./RadioGroupField";

View file

@ -0,0 +1,71 @@
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import { Theme } from "@material-ui/core/styles";
import { createStyles, makeStyles, useTheme } from "@material-ui/styles";
import React from "react";
import i18n from "../../i18n";
import { ListSettings } from "../../types";
const useStyles = makeStyles(
(theme: Theme) =>
createStyles({
label: {
fontSize: 14
},
select: {
"& div": {
"&:focus": {
background: "none"
},
color: theme.palette.primary.main,
padding: "0 10px 0 5px"
},
"& svg": {
color: theme.palette.primary.main
},
"&:after, &:before, &:hover": {
border: "none !important"
}
}
}),
{
name: "RowNumberSelect"
}
);
interface RowNumberSelectProps {
choices: number[];
className?: string;
settings: ListSettings;
onChange(key: keyof ListSettings, value: any);
}
const RowNumberSelect: React.FC<RowNumberSelectProps> = ({
className,
choices,
settings,
onChange
}) => {
const theme = useTheme();
const classes = useStyles({ theme });
return (
<div className={className}>
<span className={classes.label}>{i18n.t("No of Rows:")}</span>
<Select
className={classes.select}
value={settings.rowNumber}
onChange={event => onChange("rowNumber", event.target.value)}
>
{choices.length > 0 &&
choices.map(choice => (
<MenuItem value={choice} key={choice}>
{choice}
</MenuItem>
))}
</Select>
</div>
);
};
export default RowNumberSelect;

View file

@ -0,0 +1,2 @@
export { default } from "./RowNumberSelect";
export * from "./RowNumberSelect";

View file

@ -0,0 +1,29 @@
import { Theme } from "@material-ui/core/styles";
import TableCell from "@material-ui/core/TableCell";
import makeStyles from "@material-ui/styles/makeStyles";
import React from "react";
import { SortableHandle as SortableHandleHoc } from "react-sortable-hoc";
import Draggable from "@saleor/icons/Draggable";
const useStyles = makeStyles((theme: Theme) => ({
columnDrag: {
"&:first-child": {
paddingRight: theme.spacing.unit * 2
},
cursor: "grab",
width: 48 + theme.spacing.unit * 1.5
}
}));
const SortableHandle = SortableHandleHoc(() => {
const classes = useStyles({});
return (
<TableCell className={classes.columnDrag}>
<Draggable />
</TableCell>
);
});
export default SortableHandle;

View file

@ -0,0 +1,45 @@
import { Theme } from "@material-ui/core/styles";
import TableBody, { TableBodyProps } from "@material-ui/core/TableBody";
import makeStyles from "@material-ui/styles/makeStyles";
import React from "react";
import { SortableContainer } from "react-sortable-hoc";
import { ReorderAction } from "@saleor/types";
const InnerSortableTableBody = SortableContainer<TableBodyProps>(
({ children, ...props }) => <TableBody {...props}>{children}</TableBody>
);
export interface SortableTableBodyProps {
onSortEnd: ReorderAction;
}
const useStyles = makeStyles((theme: Theme) => ({
ghost: {
"& td": {
borderBottom: "none"
},
background: theme.palette.background.paper,
fontFamily: theme.typography.fontFamily,
fontSize: theme.overrides.MuiTableCell.root.fontSize,
opacity: 0.5
}
}));
const SortableTableBody: React.FC<
TableBodyProps & SortableTableBodyProps
> = props => {
const classes = useStyles({});
return (
<InnerSortableTableBody
helperClass={classes.ghost}
axis="y"
lockAxis="y"
useDragHandle
{...props}
/>
);
};
export default SortableTableBody;

View file

@ -0,0 +1,15 @@
import TableRow, { TableRowProps } from "@material-ui/core/TableRow";
import React from "react";
import { SortableElement } from "react-sortable-hoc";
import SortableHandle from "./SortableHandle";
const SortableTableRow = SortableElement<TableRowProps>(
({ children, ...props }) => (
<TableRow {...props}>
<SortableHandle />
{children}
</TableRow>
)
);
export default SortableTableRow;

View file

@ -0,0 +1,5 @@
export * from "./SortableTableBody";
export { default as SortableTableBody } from "./SortableTableBody";
export * from "./SortableTableRow";
export { default as SortableTableRow } from "./SortableTableRow";

View file

@ -0,0 +1,103 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import React from "react";
import CardTitle from "@saleor/components/CardTitle";
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
import Grid from "@saleor/components/Grid";
import i18n from "../../../i18n";
import { FormErrors } from "../../../types";
import { FormData } from "../VoucherDetailsPage";
interface VoucherDatesProps {
data: FormData;
defaultCurrency: string;
disabled: boolean;
errors: FormErrors<"endDate" | "startDate">;
onChange: (event: React.ChangeEvent<any>) => void;
}
const VoucherDates = ({
data,
disabled,
errors,
onChange
}: VoucherDatesProps) => {
return (
<Card>
<CardTitle title={i18n.t("Active Dates")} />
<CardContent>
<Grid variant="uniform">
<TextField
disabled={disabled}
error={!!errors.startDate}
helperText={errors.startDate}
name={"startDate" as keyof FormData}
onChange={onChange}
label={i18n.t("Start Date")}
value={data.startDate}
type="date"
InputLabelProps={{
shrink: true
}}
fullWidth
/>
<TextField
disabled={disabled}
error={!!errors.startDate}
helperText={errors.startDate}
name={"startTime" as keyof FormData}
onChange={onChange}
label={i18n.t("Start Hour")}
value={data.startTime}
type="time"
InputLabelProps={{
shrink: true
}}
fullWidth
/>
</Grid>
<ControlledCheckbox
checked={data.hasEndDate}
label={i18n.t("Set end date")}
name={"hasEndDate" as keyof FormData}
onChange={onChange}
/>
{data.hasEndDate && (
<Grid variant="uniform">
<TextField
disabled={disabled}
error={!!errors.endDate}
helperText={errors.endDate}
name={"endDate" as keyof FormData}
onChange={onChange}
label={i18n.t("End Date")}
value={data.endDate}
type="date"
InputLabelProps={{
shrink: true
}}
fullWidth
/>
<TextField
disabled={disabled}
error={!!errors.endDate}
helperText={errors.endDate}
name={"endTime" as keyof FormData}
onChange={onChange}
label={i18n.t("End Hour")}
value={data.endTime}
type="time"
InputLabelProps={{
shrink: true
}}
fullWidth
/>
</Grid>
)}
</CardContent>
</Card>
);
};
export default VoucherDates;

View file

@ -0,0 +1,2 @@
export { default } from "./VoucherDates";
export * from "./VoucherDates";

View file

@ -0,0 +1,64 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import React from "react";
import CardTitle from "@saleor/components/CardTitle";
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
import i18n from "../../../i18n";
import { FormErrors } from "../../../types";
import { FormData } from "../VoucherDetailsPage";
interface VoucherLimitsProps {
data: FormData;
defaultCurrency: string;
disabled: boolean;
errors: FormErrors<"usageLimit">;
onChange: (event: React.ChangeEvent<any>) => void;
}
const VoucherLimits = ({
data,
disabled,
errors,
onChange
}: VoucherLimitsProps) => {
return (
<Card>
<CardTitle title={i18n.t("Usage Limit ")} />
<CardContent>
<ControlledCheckbox
checked={data.hasUsageLimit}
label={i18n.t(
"Limit number of times this discount can be used in total"
)}
name={"hasUsageLimit" as keyof FormData}
onChange={onChange}
/>
{data.hasUsageLimit && (
<TextField
disabled={disabled}
error={!!errors.usageLimit}
helperText={errors.usageLimit}
label={i18n.t("Limit of Uses")}
name={"usageLimit" as keyof FormData}
value={data.usageLimit}
onChange={onChange}
type="number"
inputProps={{
min: 0
}}
fullWidth
/>
)}
<ControlledCheckbox
checked={data.applyOncePerCustomer}
label={i18n.t("Limit to one use per customer")}
name={"applyOncePerCustomer" as keyof FormData}
onChange={onChange}
/>
</CardContent>
</Card>
);
};
export default VoucherLimits;

View file

@ -0,0 +1,2 @@
export { default } from "./VoucherLimits";
export * from "./VoucherLimits";

View file

@ -0,0 +1,82 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import React from "react";
import CardTitle from "@saleor/components/CardTitle";
import { FormSpacer } from "@saleor/components/FormSpacer";
import RadioGroupField from "@saleor/components/RadioGroupField";
import { RequirementsPicker } from "@saleor/discounts/types";
import i18n from "@saleor/i18n";
import { FormErrors } from "@saleor/types";
import { FormData } from "../VoucherDetailsPage";
interface VoucherRequirementsProps {
data: FormData;
defaultCurrency: string;
disabled: boolean;
errors: FormErrors<"minAmountSpent" | "minCheckoutItemsQuantity">;
onChange: (event: React.ChangeEvent<any>) => void;
}
const VoucherRequirements = ({
data,
disabled,
errors,
onChange
}: VoucherRequirementsProps) => {
const requirementsPickerChoices = [
{
label: i18n.t("None"),
value: RequirementsPicker.NONE
},
{
label: i18n.t("Minimal order value"),
value: RequirementsPicker.ORDER
},
{
label: i18n.t("Minimum quantity of items"),
value: RequirementsPicker.ITEM
}
];
return (
<Card>
<CardTitle title={i18n.t("Minimum Requirements")} />
<CardContent>
<RadioGroupField
choices={requirementsPickerChoices}
disabled={disabled}
name={"requirementsPicker" as keyof FormData}
value={data.requirementsPicker}
onChange={onChange}
/>
<FormSpacer />
{data.requirementsPicker === RequirementsPicker.ORDER ? (
<TextField
disabled={disabled}
error={!!errors.minAmountSpent}
helperText={errors.minAmountSpent}
label={i18n.t("Minimal order value")}
name={"minAmountSpent" as keyof FormData}
value={data.minAmountSpent}
onChange={onChange}
fullWidth
/>
) : data.requirementsPicker === RequirementsPicker.ITEM ? (
<TextField
disabled={disabled}
error={!!errors.minCheckoutItemsQuantity}
helperText={errors.minCheckoutItemsQuantity}
label={i18n.t("Minimum quantity of items")}
name={"minCheckoutItemsQuantity" as keyof FormData}
value={data.minCheckoutItemsQuantity}
onChange={onChange}
fullWidth
/>
) : null}
</CardContent>
</Card>
);
};
export default VoucherRequirements;

View file

@ -0,0 +1,2 @@
export { default } from "./VoucherRequirements";
export * from "./VoucherRequirements";

View file

@ -0,0 +1,60 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import React from "react";
import CardTitle from "@saleor/components/CardTitle";
import Grid from "@saleor/components/Grid";
import RadioGroupField from "@saleor/components/RadioGroupField";
import i18n from "../../../i18n";
import { FormErrors } from "../../../types";
import { DiscountValueTypeEnum } from "../../../types/globalTypes";
import { FormData } from "../VoucherDetailsPage";
interface VoucherTypesProps {
data: FormData;
errors: FormErrors<"discountType">;
disabled: boolean;
onChange: (event: React.ChangeEvent<any>) => void;
}
const VoucherTypes = ({
data,
disabled,
errors,
onChange
}: VoucherTypesProps) => {
const voucherTypeChoices = [
{
label: i18n.t("Fixed Amount"),
value: DiscountValueTypeEnum.FIXED
},
{
label: i18n.t("Percentage"),
value: DiscountValueTypeEnum.PERCENTAGE
},
{
label: i18n.t("Free Shipping"),
value: "SHIPPING"
}
];
return (
<Card>
<CardTitle title={i18n.t("Discount Type")} />
<CardContent>
<Grid variant="uniform">
<RadioGroupField
choices={voucherTypeChoices}
disabled={disabled}
error={!!errors.discountType}
hint={errors.discountType}
name={"discountType" as keyof FormData}
value={data.discountType}
onChange={onChange}
/>
</Grid>
</CardContent>
</Card>
);
};
export default VoucherTypes;

View file

@ -0,0 +1,2 @@
export { default } from "./VoucherTypes";
export * from "./VoucherTypes";

View file

@ -0,0 +1,112 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import Typography from "@material-ui/core/Typography";
import React from "react";
import CardTitle from "@saleor/components/CardTitle";
import ControlledSwitch from "@saleor/components/ControlledSwitch";
import { FormSpacer } from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import RadioGroupField from "@saleor/components/RadioGroupField";
import TextFieldWithChoice from "@saleor/components/TextFieldWithChoice";
import i18n from "../../../i18n";
import { FormErrors } from "../../../types";
import { DiscountValueTypeEnum } from "../../../types/globalTypes";
import { translateVoucherTypes } from "../../translations";
import { FormData } from "../VoucherDetailsPage";
interface VoucherValueProps {
data: FormData;
defaultCurrency: string;
errors: FormErrors<"discountValue" | "type">;
disabled: boolean;
variant: string;
onChange: (event: React.ChangeEvent<any>) => void;
}
export enum VoucherType {
ENTIRE_ORDER = "ENTIRE_ORDER",
SPECIFIC_PRODUCT = "SPECIFIC_PRODUCT"
}
const VoucherValue = ({
data,
defaultCurrency,
disabled,
errors,
variant,
onChange
}: VoucherValueProps) => {
const translatedVoucherTypes = translateVoucherTypes();
const voucherTypeChoices = Object.values(VoucherType).map(type => ({
label: translatedVoucherTypes[type],
value: type
}));
return (
<Card>
<CardTitle title={i18n.t("Value")} />
<CardContent>
<TextFieldWithChoice
disabled={disabled}
error={!!errors.discountValue}
ChoiceProps={{
label:
data.discountType === DiscountValueTypeEnum.FIXED
? defaultCurrency
: "%",
name: "discountType" as keyof FormData,
values: null
}}
helperText={errors.discountValue}
name={"value" as keyof FormData}
onChange={onChange}
label={i18n.t("Discount Value")}
value={data.value}
type="number"
fullWidth
inputProps={{
min: 0
}}
/>
<FormSpacer />
{variant === "update" && (
<>
<RadioGroupField
choices={voucherTypeChoices}
disabled={disabled}
error={!!errors.type}
hint={errors.type}
label={i18n.t("Discount Specific Information")}
name={"type" as keyof FormData}
value={data.type}
onChange={onChange}
/>
<FormSpacer />
</>
)}
<Hr />
<FormSpacer />
<ControlledSwitch
checked={data.applyOncePerOrder}
label={
<>
{i18n.t("Only once per order", {
context: "voucher application"
})}
<Typography variant="caption">
{i18n.t(
"If this option is disabled, discount will be counted for every eligible product"
)}
</Typography>
</>
}
onChange={onChange}
name={"applyOncePerOrder" as keyof FormData}
disabled={disabled}
/>
</CardContent>
</Card>
);
};
export default VoucherValue;

View file

@ -0,0 +1,2 @@
export { default } from "./VoucherValue";
export * from "./VoucherValue";

5
src/discounts/types.ts Normal file
View file

@ -0,0 +1,5 @@
export enum RequirementsPicker {
ORDER = "ORDER",
ITEM = "ITEM",
NONE = "NONE"
}

View file

@ -0,0 +1,34 @@
import throttle from "lodash-es/throttle";
import { MutableRefObject, useEffect, useState } from "react";
function getPosition(anchor?: HTMLElement) {
if (!!anchor) {
return {
x: anchor.scrollLeft,
y: anchor.scrollTop
};
}
return {
x: 0,
y: 0
};
}
function useElementScroll(anchor: MutableRefObject<HTMLElement>) {
const [scroll, setScroll] = useState(getPosition(anchor.current));
useEffect(() => {
if (!!anchor.current) {
const handleScroll = throttle(
() => setScroll(getPosition(anchor.current)),
100
);
anchor.current.addEventListener("scroll", handleScroll);
return () => anchor.current.removeEventListener("scroll", handleScroll);
}
}, [anchor.current]);
return scroll;
}
export default useElementScroll;

148
src/hooks/useForm.ts Normal file
View file

@ -0,0 +1,148 @@
import isEqual from "lodash-es/isEqual";
import { useState } from "react";
import { UserError } from "@saleor/types";
import { toggle } from "@saleor/utils/lists";
import useStateFromProps from "./useStateFromProps";
export interface ChangeEvent<TData = any> {
target: {
name: string;
value: TData;
};
}
export type FormChange = (event: ChangeEvent, cb?: () => void) => void;
export interface UseFormResult<T> {
change: FormChange;
data: T;
errors: Record<string, string>;
hasChanged: boolean;
reset: () => void;
set: (data: T) => void;
submit: () => void;
triggerChange: () => void;
toggleValue: FormChange;
}
function parseErrors(errors: UserError[]): Record<string, string> {
return errors
? errors.reduce(
(acc, curr) =>
curr.field
? {
...acc,
[curr.field.split(":")[0]]: curr.message
}
: acc,
{}
)
: {};
}
type FormData = Record<string, any | any[]>;
function merge<T extends FormData>(prevData: T, prevState: T, data: T): T {
return Object.keys(prevState).reduce(
(acc, key) => {
if (!isEqual(data[key], prevData[key])) {
acc[key as keyof T] = data[key];
}
return acc;
},
{ ...prevState }
);
}
function handleRefresh<T extends FormData>(
data: T,
newData: T,
setChanged: (status: boolean) => void
) {
if (isEqual(data, newData)) {
setChanged(false);
}
}
function useForm<T extends FormData>(
initial: T,
errors: UserError[],
onSubmit: (data: T) => void
): UseFormResult<T> {
const [hasChanged, setChanged] = useState(false);
const [data, setData] = useStateFromProps(initial, {
mergeFunc: merge,
onRefresh: newData => handleRefresh(data, newData, setChanged)
});
function toggleValue(event: ChangeEvent, cb?: () => void) {
const { name, value } = event.target;
const field = data[name as keyof T];
if (Array.isArray(field)) {
if (!hasChanged) {
setChanged(true);
}
setData({
...data,
[name]: toggle(value, field, isEqual)
});
}
if (typeof cb === "function") {
cb();
}
}
function change(event: ChangeEvent) {
const { name, value } = event.target;
if (!(name in data)) {
console.error(`Unknown form field: ${name}`);
return;
} else {
if (data[name] !== value) {
setChanged(true);
}
setData(data => ({
...data,
[name]: value
}));
}
}
function reset() {
setData(initial);
}
function set(newData: Partial<T>) {
setData(data => ({
...data,
...newData
}));
}
function submit() {
return onSubmit(data);
}
function triggerChange() {
setChanged(true);
}
return {
change,
data,
errors: parseErrors(errors),
hasChanged,
reset,
set,
submit,
toggleValue,
triggerChange
};
}
export default useForm;

51
src/hooks/useFormset.ts Normal file
View file

@ -0,0 +1,51 @@
import useStateFromProps from "./useStateFromProps";
export type FormsetChange<TValue = any> = (id: string, value: TValue) => void;
export interface FormsetAtomicData<TData = object, TValue = any> {
data: TData;
id: string;
label: string;
value: TValue;
}
export type FormsetData<TData = object, TValue = any> = Array<
FormsetAtomicData<TData, TValue>
>;
export interface UseFormsetOutput<TData = object, TValue = any> {
change: FormsetChange<TValue>;
data: FormsetData<TData, TValue>;
get: (id: string) => FormsetAtomicData<TData, TValue>;
// Used for some rare situations like dataset change
set: (data: FormsetData<TData, TValue>) => void;
}
function useFormset<TData = object, TValue = any>(
initial: FormsetData<TData>
): UseFormsetOutput<TData> {
const [data, setData] = useStateFromProps<FormsetData<TData, TValue>>(
initial || []
);
function getItem(id: string): FormsetAtomicData<TData, TValue> {
return data.find(item => item.id === id);
}
function setItemValue(id: string, value: TValue) {
const itemIndex = data.findIndex(item => item.id === id);
setData([
...data.slice(0, itemIndex),
{
...data[itemIndex],
value
},
...data.slice(itemIndex + 1)
]);
}
return {
change: setItemValue,
data,
get: getItem,
set: setData
};
}
export default useFormset;

View file

@ -0,0 +1,47 @@
import { useEffect, useState } from "react";
function useListActions<TData>(
initial: TData[] = [],
compareFunc: (a: TData, b: TData) => boolean = (a, b) => a === b
) {
const [listElements, setListElements] = useState(initial);
useEffect(() => setListElements(initial), [JSON.stringify(initial)]);
function isSelected(data: TData) {
return !!listElements.find(listElement => compareFunc(listElement, data));
}
function add(data: TData) {
setListElements([...listElements, data]);
}
function remove(data: TData) {
setListElements(
listElements.filter(listElement => !compareFunc(listElement, data))
);
}
function reset() {
setListElements([]);
}
function toggle(data: TData) {
isSelected(data) ? remove(data) : add(data);
}
function set(data: TData[]) {
setListElements(data);
}
return {
add,
isSelected,
listElements,
remove,
reset,
set,
toggle
};
}
export default useListActions;

View file

@ -0,0 +1,30 @@
import useLocalStorage from "@saleor/hooks/useLocalStorage";
import { AppListViewSettings, defaultListSettings } from "./../config";
import { ListSettings, ListViews } from "./../types";
export interface UseListSettings<TColumns extends string = string> {
settings: ListSettings<TColumns>;
updateListSettings: (key: keyof ListSettings<TColumns>, value: any) => void;
}
export default function useListSettings<TColumns extends string = string>(
listName: ListViews
): UseListSettings<TColumns> {
const [settings, setListSettings] = useLocalStorage<AppListViewSettings>(
"listConfig",
defaultListSettings
);
const updateListSettings = (key: keyof ListSettings, value: any) =>
setListSettings(settings => ({
...settings,
[listName]: {
...settings[listName],
[key]: value
}
}));
return {
settings: settings[listName] as ListSettings<TColumns>,
updateListSettings
};
}

View file

@ -0,0 +1,21 @@
import { useState } from "react";
export type SetLocalStorageValue<T> = T | ((prevValue: T) => T);
export type SetLocalStorage<T> = (value: SetLocalStorageValue<T>) => void;
export default function useLocalStorage<T>(
key: string,
initialValue: T
): [T, SetLocalStorage<T>] {
const [storedValue, setStoredValue] = useState<T>(() => {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
});
const setValue = (value: SetLocalStorageValue<T>) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
}

View file

@ -0,0 +1,2 @@
export { default } from "./useModalDialogErrors";
export * from "./useModalDialogErrors";

View file

@ -0,0 +1,44 @@
import { renderHook } from "@testing-library/react-hooks";
import useModalDialogErrors from "./useModalDialogErrors";
const errors = ["err1", "err2"];
test("Does not render errors after close", () => {
const { result, rerender } = renderHook(
({ errors, open }) => useModalDialogErrors(errors, open),
{
initialProps: {
errors: [] as string[],
open: false
}
}
);
// Open modal
rerender({
errors: [],
open: true
});
expect(result.current.length).toBe(0);
// Throw errors
rerender({
errors,
open: true
});
expect(result.current.length).toBe(2);
// Close modal
rerender({
errors,
open: false
});
// Open modal
rerender({
errors,
open: true
});
expect(result.current.length).toBe(0);
});

View file

@ -0,0 +1,17 @@
import useModalDialogOpen from "../useModalDialogOpen";
import useStateFromProps from "../useStateFromProps";
function useModalDialogErrors<TError>(
errors: TError[],
open: boolean
): TError[] {
const [state, setState] = useStateFromProps(errors);
useModalDialogOpen(open, {
onClose: () => setState([])
});
return state;
}
export default useModalDialogErrors;

View file

@ -0,0 +1,2 @@
export { default } from "./useModalDialogOpen";
export * from "./useModalDialogOpen";

View file

@ -0,0 +1,55 @@
import { renderHook } from "@testing-library/react-hooks";
import useModalDialogOpen from "./useModalDialogOpen";
const onClose = jest.fn();
const onOpen = jest.fn();
const cbs = {
onClose,
onOpen
};
test("Does not render errors after close", () => {
const { rerender } = renderHook(
({ open, cbs }) => useModalDialogOpen(open, cbs),
{
initialProps: {
cbs,
open: false
}
}
);
// Open modal
rerender({
cbs,
open: true
});
expect(onOpen).toBeCalledTimes(1);
expect(onClose).toBeCalledTimes(0);
// Rerender modal
rerender({
cbs,
open: true
});
expect(onOpen).toBeCalledTimes(1);
expect(onClose).toBeCalledTimes(0);
// Close modal
rerender({
cbs,
open: false
});
expect(onOpen).toBeCalledTimes(1);
expect(onClose).toBeCalledTimes(1);
// Open modal
rerender({
cbs,
open: true
});
expect(onOpen).toBeCalledTimes(2);
expect(onClose).toBeCalledTimes(1);
});

View file

@ -0,0 +1,26 @@
import { useEffect, useState } from "react";
function useModalDialogOpen(
open: boolean,
cbs: {
onClose?: () => void;
onOpen?: () => void;
}
): void {
const [prevOpen, setPrevOpen] = useState(open);
useEffect(() => {
if (open !== prevOpen) {
setPrevOpen(open);
if (cbs.onOpen && open) {
cbs.onOpen();
}
if (cbs.onClose && !open) {
cbs.onClose();
}
}
}, [open]);
}
export default useModalDialogOpen;

View file

@ -0,0 +1,28 @@
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { maybe } from "@saleor/misc";
import useListActions from "./useListActions";
function useMultiAutocomplete(initial: MultiAutocompleteChoiceType[] = []) {
const { listElements, toggle } = useListActions<MultiAutocompleteChoiceType>(
initial,
(a, b) => a.value === b.value
);
const handleSelect = (
event: React.ChangeEvent<any>,
choices: MultiAutocompleteChoiceType[]
) => {
const value: string = event.target.value;
const match = choices.find(choice => choice.value === value);
toggle({
label: maybe(() => match.label, value),
value
});
};
return {
change: handleSelect,
data: listElements
};
}
export default useMultiAutocomplete;

View file

@ -0,0 +1,29 @@
import React from "react";
import { ChangeEvent } from "@saleor/hooks/useForm";
export type UseSearchQuery = [string, (event: ChangeEvent) => void, () => void];
function useSearchQuery(
onFetch: (query: string) => void,
initial?: string
): UseSearchQuery {
const [query, setQuery] = React.useState(initial || "");
const change = (event: ChangeEvent) => {
const value = event.target.value;
onFetch(value);
setQuery(value);
};
const reset = () =>
change({
target: {
name: "",
value: initial || ""
}
});
return [query, change, reset];
}
export default useSearchQuery;

View file

@ -0,0 +1,35 @@
import isEqual from "lodash-es/isEqual";
import { Dispatch, SetStateAction, useState } from "react";
export interface UseStateFromPropsOpts<T> {
mergeFunc?: (prevData: T, state: T, newData: T) => T;
onRefresh?: (data: T) => void;
}
function useStateFromProps<T>(
data: T,
opts?: UseStateFromPropsOpts<T>
): [T, Dispatch<SetStateAction<T>>] {
const [state, setState] = useState(data);
const [prevData, setPrevData] = useState(data);
if (!opts) {
opts = {};
}
const { mergeFunc, onRefresh } = opts;
const shouldUpdate = !isEqual(prevData, data);
if (shouldUpdate) {
const newData =
typeof mergeFunc === "function" ? mergeFunc(prevData, state, data) : data;
setState(newData);
setPrevData(data);
if (typeof onRefresh === "function") {
onRefresh(newData);
}
}
return [state, setState];
}
export default useStateFromProps;

View file

@ -0,0 +1,24 @@
import throttle from "lodash-es/throttle";
import { useEffect, useState } from "react";
function getPosition() {
return {
x: window.pageXOffset,
y: window.pageYOffset
};
}
function useWindowScroll() {
const [scroll, setScroll] = useState(getPosition);
useEffect(() => {
const handleScroll = throttle(() => setScroll(getPosition()), 100);
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return scroll;
}
export default useWindowScroll;

View file

@ -0,0 +1,192 @@
import Button from "@material-ui/core/Button";
import CircularProgress from "@material-ui/core/CircularProgress";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import { Theme } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import makeStyles from "@material-ui/styles/makeStyles";
import React from "react";
import InfiniteScroll from "react-infinite-scroller";
import Checkbox from "@saleor/components/Checkbox";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
import useSearchQuery from "@saleor/hooks/useSearchQuery";
import i18n from "@saleor/i18n";
import { maybe, renderCollection } from "@saleor/misc";
import { FetchMoreProps } from "@saleor/types";
import { SearchAttributes_productType_availableAttributes_edges_node } from "../../containers/SearchAttributes/types/SearchAttributes";
const useStyles = makeStyles((theme: Theme) => ({
checkboxCell: {
paddingLeft: 0
},
loadMoreLoaderContainer: {
alignItems: "center",
display: "flex",
height: theme.spacing.unit * 3,
justifyContent: "center"
},
scrollArea: {
overflowY: "scroll"
},
wideCell: {
width: "100%"
}
}));
export interface AssignAttributeDialogProps extends FetchMoreProps {
confirmButtonState: ConfirmButtonTransitionState;
errors: string[];
open: boolean;
attributes: SearchAttributes_productType_availableAttributes_edges_node[];
selected: string[];
onClose: () => void;
onOpen: () => void;
onSubmit: () => void;
onToggle: (id: string) => void;
}
const AssignAttributeDialog: React.FC<AssignAttributeDialogProps> = ({
attributes,
confirmButtonState,
errors: apiErrors,
hasMore,
loading,
open,
selected,
onClose,
onFetch,
onFetchMore,
onOpen,
onSubmit,
onToggle
}: AssignAttributeDialogProps) => {
const classes = useStyles({});
const [query, onQueryChange, resetQuery] = useSearchQuery(onFetch);
const errors = useModalDialogErrors(apiErrors, open);
useModalDialogOpen(open, {
onClose: resetQuery,
onOpen
});
return (
<Dialog onClose={onClose} open={open} fullWidth maxWidth="sm">
<DialogTitle>{i18n.t("Assign Attribute")}</DialogTitle>
<DialogContent>
<TextField
name="query"
value={query}
onChange={onQueryChange}
label={i18n.t("Search Attributes", {
context: "attribute search input label"
})}
placeholder={i18n.t("Search by attribute name", {
context: "attribute search input placeholder"
})}
fullWidth
InputProps={{
autoComplete: "off",
endAdornment: loading && <CircularProgress size={16} />
}}
/>
</DialogContent>
<DialogContent className={classes.scrollArea}>
<InfiniteScroll
pageStart={0}
loadMore={onFetchMore}
hasMore={hasMore}
useWindow={false}
loader={
<div className={classes.loadMoreLoaderContainer}>
<CircularProgress size={16} />
</div>
}
threshold={100}
key="infinite-scroll"
>
<Table key="table">
<TableBody>
{renderCollection(
attributes,
attribute => {
if (!attribute) {
return null;
}
const isChecked = !!selected.find(
selectedAttribute => selectedAttribute === attribute.id
);
return (
<TableRow key={maybe(() => attribute.id)}>
<TableCell
padding="checkbox"
className={classes.checkboxCell}
>
<Checkbox
checked={isChecked}
onChange={() => onToggle(attribute.id)}
/>
</TableCell>
<TableCell className={classes.wideCell}>
{attribute.name}
<Typography variant="caption">
{attribute.slug}
</Typography>
</TableCell>
</TableRow>
);
},
() =>
!loading && (
<TableRow>
<TableCell colSpan={2}>
{i18n.t("No results found")}
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</InfiniteScroll>
</DialogContent>
{errors.length > 0 && (
<DialogContent>
{errors.map((error, errorIndex) => (
<DialogContentText color="error" key={errorIndex}>
{error}
</DialogContentText>
))}
</DialogContent>
)}
<DialogActions>
<Button onClick={onClose}>
{i18n.t("Cancel", { context: "button" })}
</Button>
<ConfirmButton
transitionState={confirmButtonState}
color="primary"
variant="contained"
type="submit"
onClick={onSubmit}
>
{i18n.t("Assign attributes", { context: "button" })}
</ConfirmButton>
</DialogActions>
</Dialog>
);
};
AssignAttributeDialog.displayName = "AssignAttributeDialog";
export default AssignAttributeDialog;

View file

@ -0,0 +1,2 @@
export { default } from './AssignAttributeDialog';
export * from './AssignAttributeDialog';

View file

@ -0,0 +1,49 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import i18n from "@saleor/i18n";
export interface ProductTypeAttributeUnassignDialogProps {
attributeName: string;
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
productTypeName: string;
onClose: () => void;
onConfirm: () => void;
}
const ProductTypeAttributeUnassignDialog: React.FC<
ProductTypeAttributeUnassignDialogProps
> = ({
attributeName,
confirmButtonState,
open,
productTypeName,
onClose,
onConfirm
}) => (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={onConfirm}
title={i18n.t("Unassign attribute from product type")}
>
<DialogContentText
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to unassign <strong>{{ attributeName }}</strong> from <strong>{{ productTypeName }}</strong>?",
{
attributeName,
productTypeName
}
)
}}
/>
</ActionDialog>
);
ProductTypeAttributeUnassignDialog.displayName =
"ProductTypeAttributeUnassignDialog";
export default ProductTypeAttributeUnassignDialog;

View file

@ -0,0 +1,2 @@
export { default } from './ProductTypeAttributeUnassignDialog';
export * from './ProductTypeAttributeUnassignDialog';

View file

@ -0,0 +1,49 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import i18n from "@saleor/i18n";
export interface ProductTypeBulkAttributeUnassignDialogProps {
attributeQuantity: string;
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
productTypeName: string;
onClose: () => void;
onConfirm: () => void;
}
const ProductTypeBulkAttributeUnassignDialog: React.FC<
ProductTypeBulkAttributeUnassignDialogProps
> = ({
attributeQuantity,
confirmButtonState,
open,
productTypeName,
onClose,
onConfirm
}) => (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={onConfirm}
title={i18n.t("Unassign attribute from product type")}
>
<DialogContentText
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to unassign <strong>{{ attributeQuantity }}</strong> attributes from <strong>{{ productTypeName }}</strong>?",
{
attributeQuantity,
productTypeName
}
)
}}
/>
</ActionDialog>
);
ProductTypeBulkAttributeUnassignDialog.displayName =
"ProductTypeBulkAttributeUnassignDialog";
export default ProductTypeBulkAttributeUnassignDialog;

View file

@ -0,0 +1,2 @@
export { default } from './ProductTypeBulkAttributeUnassignDialog';
export * from './ProductTypeBulkAttributeUnassignDialog';

View file

@ -0,0 +1,43 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import i18n from "@saleor/i18n";
export interface ProductTypeDeleteDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
name: string;
onClose: () => void;
onConfirm: () => void;
}
const ProductTypeDeleteDialog: React.FC<ProductTypeDeleteDialogProps> = ({
confirmButtonState,
open,
name,
onClose,
onConfirm
}) => (
<ActionDialog
confirmButtonState={confirmButtonState}
open={open}
onClose={onClose}
onConfirm={onConfirm}
title={i18n.t("Remove product type")}
>
<DialogContentText
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to remove <strong>{{ name }}</strong>?",
{
name
}
)
}}
/>
</ActionDialog>
);
ProductTypeDeleteDialog.displayName = "ProductTypeDeleteDialog";
export default ProductTypeDeleteDialog;

View file

@ -0,0 +1,2 @@
export { default } from './ProductTypeDeleteDialog';
export * from './ProductTypeDeleteDialog';

View file

@ -0,0 +1,42 @@
import gql from "graphql-tag";
import { pageInfoFragment } from "@saleor/queries";
import BaseSearch from "../../../containers/BaseSearch";
import {
SearchAttributes,
SearchAttributesVariables
} from "./types/SearchAttributes";
export const searchAttributes = gql`
${pageInfoFragment}
query SearchAttributes(
$id: ID!
$after: String
$first: Int!
$query: String!
) {
productType(id: $id) {
id
availableAttributes(
after: $after
first: $first
filter: { search: $query }
) {
edges {
node {
id
name
slug
}
}
pageInfo {
...PageInfoFragment
}
}
}
}
`;
export default BaseSearch<SearchAttributes, SearchAttributesVariables>(
searchAttributes
);

View file

@ -0,0 +1,50 @@
/* 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;
}

View file

@ -0,0 +1,74 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeAssignInput } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AssignAttribute
// ====================================================
export interface AssignAttribute_attributeAssign_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AssignAttribute_attributeAssign_productType_taxType {
__typename: "TaxType";
description: string | null;
taxCode: string | null;
}
export interface AssignAttribute_attributeAssign_productType_productAttributes {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
}
export interface AssignAttribute_attributeAssign_productType_variantAttributes {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
}
export interface AssignAttribute_attributeAssign_productType_weight {
__typename: "Weight";
unit: string;
value: number;
}
export interface AssignAttribute_attributeAssign_productType {
__typename: "ProductType";
id: string;
name: string;
hasVariants: boolean;
isShippingRequired: boolean;
taxType: AssignAttribute_attributeAssign_productType_taxType | null;
productAttributes: (AssignAttribute_attributeAssign_productType_productAttributes | null)[] | null;
variantAttributes: (AssignAttribute_attributeAssign_productType_variantAttributes | null)[] | null;
weight: AssignAttribute_attributeAssign_productType_weight | null;
}
export interface AssignAttribute_attributeAssign {
__typename: "AttributeAssign";
errors: AssignAttribute_attributeAssign_errors[] | null;
productType: AssignAttribute_attributeAssign_productType | null;
}
export interface AssignAttribute {
attributeAssign: AssignAttribute_attributeAssign | null;
}
export interface AssignAttributeVariables {
id: string;
operations: AttributeAssignInput[];
}

View file

@ -0,0 +1,75 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ReorderInput, AttributeTypeEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: ProductTypeAttributeReorder
// ====================================================
export interface ProductTypeAttributeReorder_productTypeReorderAttributes_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType_taxType {
__typename: "TaxType";
description: string | null;
taxCode: string | null;
}
export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType_productAttributes {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
}
export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType_variantAttributes {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
}
export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType_weight {
__typename: "Weight";
unit: string;
value: number;
}
export interface ProductTypeAttributeReorder_productTypeReorderAttributes_productType {
__typename: "ProductType";
id: string;
name: string;
hasVariants: boolean;
isShippingRequired: boolean;
taxType: ProductTypeAttributeReorder_productTypeReorderAttributes_productType_taxType | null;
productAttributes: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_productAttributes | null)[] | null;
variantAttributes: (ProductTypeAttributeReorder_productTypeReorderAttributes_productType_variantAttributes | null)[] | null;
weight: ProductTypeAttributeReorder_productTypeReorderAttributes_productType_weight | null;
}
export interface ProductTypeAttributeReorder_productTypeReorderAttributes {
__typename: "ProductTypeReorderAttributes";
errors: ProductTypeAttributeReorder_productTypeReorderAttributes_errors[] | null;
productType: ProductTypeAttributeReorder_productTypeReorderAttributes_productType | null;
}
export interface ProductTypeAttributeReorder {
productTypeReorderAttributes: ProductTypeAttributeReorder_productTypeReorderAttributes | null;
}
export interface ProductTypeAttributeReorderVariables {
move: ReorderInput;
productTypeId: string;
type: AttributeTypeEnum;
}

View file

@ -0,0 +1,72 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL mutation operation: UnassignAttribute
// ====================================================
export interface UnassignAttribute_attributeUnassign_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface UnassignAttribute_attributeUnassign_productType_taxType {
__typename: "TaxType";
description: string | null;
taxCode: string | null;
}
export interface UnassignAttribute_attributeUnassign_productType_productAttributes {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
}
export interface UnassignAttribute_attributeUnassign_productType_variantAttributes {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
}
export interface UnassignAttribute_attributeUnassign_productType_weight {
__typename: "Weight";
unit: string;
value: number;
}
export interface UnassignAttribute_attributeUnassign_productType {
__typename: "ProductType";
id: string;
name: string;
hasVariants: boolean;
isShippingRequired: boolean;
taxType: UnassignAttribute_attributeUnassign_productType_taxType | null;
productAttributes: (UnassignAttribute_attributeUnassign_productType_productAttributes | null)[] | null;
variantAttributes: (UnassignAttribute_attributeUnassign_productType_variantAttributes | null)[] | null;
weight: UnassignAttribute_attributeUnassign_productType_weight | null;
}
export interface UnassignAttribute_attributeUnassign {
__typename: "AttributeUnassign";
errors: UnassignAttribute_attributeUnassign_errors[] | null;
productType: UnassignAttribute_attributeUnassign_productType | null;
}
export interface UnassignAttribute {
attributeUnassign: UnassignAttribute_attributeUnassign | null;
}
export interface UnassignAttributeVariables {
id: string;
ids: (string | null)[];
}

View file

@ -0,0 +1,214 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import { Theme } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import makeStyles from "@material-ui/styles/makeStyles";
import classNames from "classnames";
import React from "react";
import CardTitle from "@saleor/components/CardTitle";
import Grid from "@saleor/components/Grid";
import Hr from "@saleor/components/Hr";
import MultiAutocompleteSelectField, {
MultiAutocompleteChoiceType
} from "@saleor/components/MultiAutocompleteSelectField";
import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType
} from "@saleor/components/SingleAutocompleteSelectField";
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
import i18n from "@saleor/i18n";
import { maybe } from "@saleor/misc";
import { ProductDetails_product_attributes_attribute_values } from "@saleor/products/types/ProductDetails";
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
export interface ProductAttributeInputData {
inputType: AttributeInputTypeEnum;
isRequired: boolean;
values: ProductDetails_product_attributes_attribute_values[];
}
export type ProductAttributeInput = FormsetAtomicData<
ProductAttributeInputData,
string[]
>;
export interface ProductAttributesProps {
attributes: ProductAttributeInput[];
disabled: boolean;
onChange: FormsetChange;
onMultiChange: FormsetChange;
}
const useStyles = makeStyles((theme: Theme) => ({
attributeSection: {
"&:last-of-type": {
paddingBottom: 0
},
padding: `${theme.spacing.unit * 2}px 0`
},
attributeSectionLabel: {
alignItems: "center",
display: "flex"
},
card: {
overflow: "visible"
},
cardContent: {
"&:last-child": {
paddingBottom: theme.spacing.unit
},
paddingTop: theme.spacing.unit
},
expansionBar: {
display: "flex"
},
expansionBarButton: {
marginBottom: theme.spacing.unit
},
expansionBarButtonIcon: {
transition: theme.transitions.duration.short + "ms"
},
expansionBarLabel: {
color: theme.palette.text.disabled,
fontSize: 14
},
expansionBarLabelContainer: {
alignItems: "center",
display: "flex",
flex: 1
},
rotate: {
transform: "rotate(180deg)"
}
}));
function getMultiChoices(
values: ProductDetails_product_attributes_attribute_values[]
): MultiAutocompleteChoiceType[] {
return values.map(value => ({
label: value.name,
value: value.slug
}));
}
function getMultiDisplayValue(
attribute: ProductAttributeInput
): MultiAutocompleteChoiceType[] {
return attribute.value.map(attributeValue => {
const definedAttributeValue = attribute.data.values.find(
definedValue => definedValue.slug === attributeValue
);
if (!!definedAttributeValue) {
return {
label: definedAttributeValue.name,
value: definedAttributeValue.slug
};
}
return {
label: attributeValue,
value: attributeValue
};
});
}
function getSingleChoices(
values: ProductDetails_product_attributes_attribute_values[]
): SingleAutocompleteChoiceType[] {
return values.map(value => ({
label: value.name,
value: value.slug
}));
}
const ProductAttributes: React.FC<ProductAttributesProps> = ({
attributes,
disabled,
onChange,
onMultiChange
}) => {
const classes = useStyles({});
const [expanded, setExpansionStatus] = React.useState(true);
const toggleExpansion = () => setExpansionStatus(!expanded);
return (
<Card className={classes.card}>
<CardTitle title={i18n.t("Attributes")} />
<CardContent className={classes.cardContent}>
<div className={classes.expansionBar}>
<div className={classes.expansionBarLabelContainer}>
<Typography className={classes.expansionBarLabel} variant="caption">
{i18n.t("{{ number }} Attributes", {
context: "number of attribute",
number: attributes.length
})}
</Typography>
</div>
<IconButton
className={classes.expansionBarButton}
onClick={toggleExpansion}
>
<ArrowDropDownIcon
className={classNames(classes.expansionBarButtonIcon, {
[classes.rotate]: expanded
})}
/>
</IconButton>
</div>
{expanded && attributes.length > 0 && (
<>
<Hr />
{attributes.map((attribute, attributeIndex) => (
<React.Fragment key={attribute.id}>
{attributeIndex > 0 && <Hr />}
<Grid className={classes.attributeSection} variant="uniform">
<div className={classes.attributeSectionLabel}>
<Typography>{attribute.label}</Typography>
</div>
<div>
{attribute.data.inputType ===
AttributeInputTypeEnum.DROPDOWN ? (
<SingleAutocompleteSelectField
choices={getSingleChoices(attribute.data.values)}
disabled={disabled}
displayValue={maybe(
() =>
attribute.data.values.find(
value => value.slug === attribute.value[0]
).name,
attribute.value[0]
)}
emptyOption
name={`attribute:${attribute.label}`}
label={i18n.t("Value")}
value={attribute.value[0]}
onChange={event =>
onChange(attribute.id, event.target.value)
}
allowCustomValues={!attribute.data.isRequired}
/>
) : (
<MultiAutocompleteSelectField
choices={getMultiChoices(attribute.data.values)}
displayValues={getMultiDisplayValue(attribute)}
label={i18n.t("Values")}
name={`attribute:${attribute.label}`}
value={attribute.value}
onChange={event =>
onMultiChange(attribute.id, event.target.value)
}
allowCustomValues={!attribute.data.isRequired}
/>
)}
</div>
</Grid>
</React.Fragment>
))}
</>
)}
</CardContent>
</Card>
);
};
ProductAttributes.displayName = "ProductAttributes";
export default ProductAttributes;

View file

@ -0,0 +1,2 @@
export { default } from './ProductAttributes';
export * from './ProductAttributes';

View file

@ -0,0 +1,133 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import { Theme } from "@material-ui/core/styles";
import AddIcon from "@material-ui/icons/Add";
import makeStyles from "@material-ui/styles/makeStyles";
import React from "react";
import { CategoryDetails_category_products_edges_node } from "@saleor/categories/types/CategoryDetails";
import ColumnPicker, {
ColumnPickerChoice
} from "@saleor/components/ColumnPicker";
import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader";
import ProductList from "@saleor/components/ProductList";
import { ProductListColumns } from "@saleor/config";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import i18n from "@saleor/i18n";
import { FilterPageProps, ListActions, PageListProps } from "@saleor/types";
import { toggle } from "@saleor/utils/lists";
import { ProductListUrlFilters } from "../../urls";
import ProductListFilter from "../ProductListFilter";
export interface ProductListPageProps
extends PageListProps<ProductListColumns>,
ListActions,
FilterPageProps<ProductListUrlFilters> {
currencySymbol: string;
products: CategoryDetails_category_products_edges_node[];
}
const useStyles = makeStyles((theme: Theme) => ({
columnPicker: {
marginRight: theme.spacing.unit * 3
}
}));
export const ProductListPage: React.FC<ProductListPageProps> = props => {
const {
currencySymbol,
currentTab,
defaultSettings,
filtersList,
filterTabs,
initialSearch,
settings,
onAdd,
onAll,
onSearchChange,
onFilterAdd,
onFilterSave,
onTabChange,
onFilterDelete,
onUpdateListSettings,
...listProps
} = props;
const classes = useStyles(props);
const [selectedColumns, setSelectedColumns] = useStateFromProps(
settings.columns
);
const handleCancel = React.useCallback(
() => setSelectedColumns(settings.columns),
[settings.columns]
);
const handleColumnToggle = (column: ProductListColumns) =>
setSelectedColumns(prevSelectedColumns =>
toggle(column, prevSelectedColumns, (a, b) => a === b)
);
const handleReset = () => setSelectedColumns(defaultSettings.columns);
const handleSave = () => onUpdateListSettings("columns", selectedColumns);
const columns: ColumnPickerChoice[] = [
{
label: i18n.t("Published"),
value: "isPublished" as ProductListColumns
},
{
label: i18n.t("Price"),
value: "price" as ProductListColumns
},
{
label: i18n.t("Type"),
value: "productType" as ProductListColumns
}
];
return (
<Container>
<PageHeader title={i18n.t("Products")}>
<ColumnPicker
className={classes.columnPicker}
columns={columns}
selectedColumns={selectedColumns}
onColumnToggle={handleColumnToggle}
onCancel={handleCancel}
onReset={handleReset}
onSave={handleSave}
/>
<Button onClick={onAdd} color="primary" variant="contained">
{i18n.t("Add product")} <AddIcon />
</Button>
</PageHeader>
<Card>
<ProductListFilter
allTabLabel={i18n.t("All Products")}
currencySymbol={currencySymbol}
currentTab={currentTab}
filterLabel={i18n.t("Select all products where:")}
filterTabs={filterTabs}
filtersList={filtersList}
initialSearch={initialSearch}
searchPlaceholder={i18n.t("Search Products...")}
onAll={onAll}
onSearchChange={onSearchChange}
onFilterAdd={onFilterAdd}
onFilterSave={onFilterSave}
onTabChange={onTabChange}
onFilterDelete={onFilterDelete}
/>
<ProductList
{...listProps}
settings={{ ...settings, columns: selectedColumns }}
onUpdateListSettings={onUpdateListSettings}
/>
</Card>
</Container>
);
};
ProductListPage.displayName = "ProductListPage";
export default ProductListPage;

View file

@ -0,0 +1,2 @@
export { default } from "./ProductListPage";
export * from "./ProductListPage";

224
src/products/utils/data.ts Normal file
View file

@ -0,0 +1,224 @@
import { RawDraftContentState } from "draft-js";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { maybe } from "@saleor/misc";
import {
ProductDetails_product,
ProductDetails_product_collections,
ProductDetails_product_variants
} from "@saleor/products/types/ProductDetails";
import { UserError } from "@saleor/types";
import { ProductAttributeInput } from "../components/ProductAttributes";
import { VariantAttributeInput } from "../components/ProductVariantAttributes";
import { ProductCreateData_productTypes_edges_node_productAttributes } from "../types/ProductCreateData";
import {
ProductVariant,
ProductVariant_attributes_attribute
} from "../types/ProductVariant";
import {
ProductVariantCreateData_product,
ProductVariantCreateData_product_productType_variantAttributes
} from "../types/ProductVariantCreateData";
export interface Collection {
id: string;
label: string;
}
interface Node {
id: string;
name: string;
}
export interface ProductType {
hasVariants: boolean;
id: string;
name: string;
productAttributes: ProductCreateData_productTypes_edges_node_productAttributes[];
}
export function getAttributeInputFromProduct(
product: ProductDetails_product
): ProductAttributeInput[] {
return maybe(
(): ProductAttributeInput[] =>
product.attributes.map(attribute => ({
data: {
inputType: attribute.attribute.inputType,
isRequired: attribute.attribute.valueRequired,
values: attribute.attribute.values
},
id: attribute.attribute.id,
label: attribute.attribute.name,
value: attribute.values.map(value => value.slug)
})),
[]
);
}
export interface ProductAttributeValueChoices {
id: string;
values: MultiAutocompleteChoiceType[];
}
export function getSelectedAttributesFromProduct(
product: ProductDetails_product
): ProductAttributeValueChoices[] {
return maybe(
() =>
product.attributes.map(attribute => ({
id: attribute.attribute.id,
values: attribute.values.map(value => ({
label: value.name,
value: value.slug
}))
})),
[]
);
}
export function getAttributeInputFromProductType(
productType: ProductType
): ProductAttributeInput[] {
return productType.productAttributes.map(attribute => ({
data: {
inputType: attribute.inputType,
isRequired: attribute.valueRequired,
values: attribute.values
},
id: attribute.id,
label: attribute.name,
value: []
}));
}
export function getAttributeInputFromVariant(
product: ProductVariant
): VariantAttributeInput[] {
return maybe(
(): VariantAttributeInput[] =>
product.attributes.map(attribute => ({
data: {
values: attribute.attribute.values
},
id: attribute.attribute.id,
label: attribute.attribute.name,
value: attribute.value.slug
})),
[]
);
}
export function getVariantAttributeInputFromProduct(
product: ProductVariantCreateData_product
): VariantAttributeInput[] {
return maybe(() =>
product.productType.variantAttributes.map(attribute => ({
data: {
values: attribute.values
},
id: attribute.id,
label: attribute.name,
value: ""
}))
);
}
export function getCollectionInput(
productCollections: ProductDetails_product_collections[]
): Collection[] {
return maybe(
() =>
productCollections.map(collection => ({
id: collection.id,
label: collection.name
})),
[]
);
}
export function getChoices(nodes: Node[]): SingleAutocompleteChoiceType[] {
return maybe(
() =>
nodes.map(node => ({
label: node.name,
value: node.id
})),
[]
);
}
export interface ProductUpdatePageFormData {
basePrice: number;
category: string | null;
collections: string[];
chargeTaxes: boolean;
description: RawDraftContentState;
isPublished: boolean;
name: string;
publicationDate: string;
seoDescription: string;
seoTitle: string;
sku: string;
stockQuantity: number;
}
export function getProductUpdatePageFormData(
product: ProductDetails_product,
variants: ProductDetails_product_variants[]
): ProductUpdatePageFormData {
return {
basePrice: maybe(() => product.basePrice.amount, 0),
category: maybe(() => product.category.id, ""),
chargeTaxes: maybe(() => product.chargeTaxes, false),
collections: maybe(
() => product.collections.map(collection => collection.id),
[]
),
description: maybe(() => JSON.parse(product.descriptionJson)),
isPublished: maybe(() => product.isPublished, false),
name: maybe(() => product.name, ""),
publicationDate: maybe(() => product.publicationDate, ""),
seoDescription: maybe(() => product.seoDescription, ""),
seoTitle: maybe(() => product.seoTitle, ""),
sku: maybe(() =>
product.productType.hasVariants
? undefined
: variants && variants[0]
? variants[0].sku
: undefined
),
stockQuantity: maybe(() =>
product.productType.hasVariants
? undefined
: variants && variants[0]
? variants[0].quantity
: undefined
)
};
}
export function getVariantAttributeErrors(
errors: UserError[],
variantAttributes: Array<
| ProductVariantCreateData_product_productType_variantAttributes
| ProductVariant_attributes_attribute
>
): Record<string, string> {
return maybe(
() =>
errors.reduce((acc, err) => {
const slug = err.field.split(":")[1];
const attribute = variantAttributes.find(
attribute => attribute.slug === slug
);
if (!!attribute) {
acc[attribute.id] = err.message;
}
return acc;
}, {}),
{}
);
}

Some files were not shown because too many files have changed in this diff Show more