Add missing files
This commit is contained in:
parent
80371c0c56
commit
a2dd1a1404
129 changed files with 7183 additions and 0 deletions
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AttributeBulkDeleteDialog';
|
||||
export * from './AttributeBulkDeleteDialog';
|
|
@ -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;
|
2
src/attributes/components/AttributeDeleteDialog/index.ts
Normal file
2
src/attributes/components/AttributeDeleteDialog/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AttributeDeleteDialog';
|
||||
export * from './AttributeDeleteDialog';
|
|
@ -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 don’t 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;
|
2
src/attributes/components/AttributeDetails/index.ts
Normal file
2
src/attributes/components/AttributeDetails/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AttributeDetails';
|
||||
export * from './AttributeDetails';
|
183
src/attributes/components/AttributeList/AttributeList.tsx
Normal file
183
src/attributes/components/AttributeList/AttributeList.tsx
Normal 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;
|
2
src/attributes/components/AttributeList/index.ts
Normal file
2
src/attributes/components/AttributeList/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AttributeList';
|
||||
export * from './AttributeList';
|
|
@ -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;
|
2
src/attributes/components/AttributeListPage/index.ts
Normal file
2
src/attributes/components/AttributeListPage/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AttributeListPage';
|
||||
export * from './AttributeListPage';
|
160
src/attributes/components/AttributePage/AttributePage.tsx
Normal file
160
src/attributes/components/AttributePage/AttributePage.tsx
Normal 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;
|
2
src/attributes/components/AttributePage/index.ts
Normal file
2
src/attributes/components/AttributePage/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AttributePage';
|
||||
export * from './AttributePage';
|
|
@ -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, you’ll be able to use this attribute to filter products in product list."
|
||||
)}
|
||||
</Typography>
|
||||
}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
AttributeProperties.displayName = "AttributeProperties";
|
||||
export default AttributeProperties;
|
2
src/attributes/components/AttributeProperties/index.ts
Normal file
2
src/attributes/components/AttributeProperties/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AttributeProperties';
|
||||
export * from './AttributeProperties';
|
|
@ -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 won’t 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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AttributeValueDeleteDialog';
|
||||
export * from './AttributeValueDeleteDialog';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AttributeValueEditDialog';
|
||||
export * from './AttributeValueEditDialog';
|
129
src/attributes/components/AttributeValues/AttributeValues.tsx
Normal file
129
src/attributes/components/AttributeValues/AttributeValues.tsx
Normal 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;
|
2
src/attributes/components/AttributeValues/index.ts
Normal file
2
src/attributes/components/AttributeValues/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AttributeValues';
|
||||
export * from './AttributeValues';
|
512
src/attributes/fixtures.ts
Normal file
512
src/attributes/fixtures.ts
Normal 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
55
src/attributes/index.tsx
Normal 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
182
src/attributes/mutations.ts
Normal 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
86
src/attributes/queries.ts
Normal 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);
|
26
src/attributes/types/AttributeBulkDelete.ts
Normal file
26
src/attributes/types/AttributeBulkDelete.ts
Normal 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[];
|
||||
}
|
52
src/attributes/types/AttributeCreate.ts
Normal file
52
src/attributes/types/AttributeCreate.ts
Normal 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;
|
||||
}
|
26
src/attributes/types/AttributeDelete.ts
Normal file
26
src/attributes/types/AttributeDelete.ts
Normal 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;
|
||||
}
|
40
src/attributes/types/AttributeDetails.ts
Normal file
40
src/attributes/types/AttributeDetails.ts
Normal 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;
|
||||
}
|
32
src/attributes/types/AttributeDetailsFragment.ts
Normal file
32
src/attributes/types/AttributeDetailsFragment.ts
Normal 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;
|
||||
}
|
17
src/attributes/types/AttributeFragment.ts
Normal file
17
src/attributes/types/AttributeFragment.ts
Normal 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;
|
||||
}
|
50
src/attributes/types/AttributeList.ts
Normal file
50
src/attributes/types/AttributeList.ts
Normal 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;
|
||||
}
|
53
src/attributes/types/AttributeUpdate.ts
Normal file
53
src/attributes/types/AttributeUpdate.ts
Normal 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;
|
||||
}
|
53
src/attributes/types/AttributeValueCreate.ts
Normal file
53
src/attributes/types/AttributeValueCreate.ts
Normal 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;
|
||||
}
|
52
src/attributes/types/AttributeValueDelete.ts
Normal file
52
src/attributes/types/AttributeValueDelete.ts
Normal 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;
|
||||
}
|
41
src/attributes/types/AttributeValueReorder.ts
Normal file
41
src/attributes/types/AttributeValueReorder.ts
Normal 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;
|
||||
}
|
53
src/attributes/types/AttributeValueUpdate.ts
Normal file
53
src/attributes/types/AttributeValueUpdate.ts
Normal 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
38
src/attributes/urls.ts
Normal 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);
|
206
src/attributes/views/AttributeCreate/AttributeCreate.tsx
Normal file
206
src/attributes/views/AttributeCreate/AttributeCreate.tsx
Normal 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;
|
2
src/attributes/views/AttributeCreate/index.ts
Normal file
2
src/attributes/views/AttributeCreate/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./AttributeCreate";
|
||||
export { default } from "./AttributeCreate";
|
360
src/attributes/views/AttributeDetails/AttributeDetails.tsx
Normal file
360
src/attributes/views/AttributeDetails/AttributeDetails.tsx
Normal 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;
|
2
src/attributes/views/AttributeDetails/index.ts
Normal file
2
src/attributes/views/AttributeDetails/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./AttributeDetails";
|
||||
export { default } from "./AttributeDetails";
|
137
src/attributes/views/AttributeList/AttributeList.tsx
Normal file
137
src/attributes/views/AttributeList/AttributeList.tsx
Normal 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;
|
2
src/attributes/views/AttributeList/index.ts
Normal file
2
src/attributes/views/AttributeList/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./AttributeList";
|
||||
export { default } from "./AttributeList";
|
103
src/components/ColumnPicker/ColumnPicker.tsx
Normal file
103
src/components/ColumnPicker/ColumnPicker.tsx
Normal 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;
|
67
src/components/ColumnPicker/ColumnPickerButton.tsx
Normal file
67
src/components/ColumnPicker/ColumnPickerButton.tsx
Normal 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;
|
128
src/components/ColumnPicker/ColumnPickerContent.tsx
Normal file
128
src/components/ColumnPicker/ColumnPickerContent.tsx
Normal 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;
|
4
src/components/ColumnPicker/index.ts
Normal file
4
src/components/ColumnPicker/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export { default } from "./ColumnPicker";
|
||||
export * from "./ColumnPicker";
|
||||
export * from "./ColumnPickerButton";
|
||||
export * from "./ColumnPickerContent";
|
95
src/components/RadioGroupField/RadioGroupField.tsx
Normal file
95
src/components/RadioGroupField/RadioGroupField.tsx
Normal 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;
|
2
src/components/RadioGroupField/index.ts
Normal file
2
src/components/RadioGroupField/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./RadioGroupField";
|
||||
export * from "./RadioGroupField";
|
71
src/components/RowNumberSelect/RowNumberSelect.tsx
Normal file
71
src/components/RowNumberSelect/RowNumberSelect.tsx
Normal 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;
|
2
src/components/RowNumberSelect/index.ts
Normal file
2
src/components/RowNumberSelect/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./RowNumberSelect";
|
||||
export * from "./RowNumberSelect";
|
29
src/components/SortableTable/SortableHandle.tsx
Normal file
29
src/components/SortableTable/SortableHandle.tsx
Normal 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;
|
45
src/components/SortableTable/SortableTableBody.tsx
Normal file
45
src/components/SortableTable/SortableTableBody.tsx
Normal 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;
|
15
src/components/SortableTable/SortableTableRow.tsx
Normal file
15
src/components/SortableTable/SortableTableRow.tsx
Normal 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;
|
5
src/components/SortableTable/index.ts
Normal file
5
src/components/SortableTable/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export * from "./SortableTableBody";
|
||||
export { default as SortableTableBody } from "./SortableTableBody";
|
||||
|
||||
export * from "./SortableTableRow";
|
||||
export { default as SortableTableRow } from "./SortableTableRow";
|
103
src/discounts/components/VoucherDates/VoucherDates.tsx
Normal file
103
src/discounts/components/VoucherDates/VoucherDates.tsx
Normal 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;
|
2
src/discounts/components/VoucherDates/index.ts
Normal file
2
src/discounts/components/VoucherDates/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./VoucherDates";
|
||||
export * from "./VoucherDates";
|
64
src/discounts/components/VoucherLimits/VoucherLimits.tsx
Normal file
64
src/discounts/components/VoucherLimits/VoucherLimits.tsx
Normal 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;
|
2
src/discounts/components/VoucherLimits/index.ts
Normal file
2
src/discounts/components/VoucherLimits/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./VoucherLimits";
|
||||
export * from "./VoucherLimits";
|
|
@ -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;
|
2
src/discounts/components/VoucherRequirements/index.ts
Normal file
2
src/discounts/components/VoucherRequirements/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./VoucherRequirements";
|
||||
export * from "./VoucherRequirements";
|
60
src/discounts/components/VoucherTypes/VoucherTypes.tsx
Normal file
60
src/discounts/components/VoucherTypes/VoucherTypes.tsx
Normal 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;
|
2
src/discounts/components/VoucherTypes/index.ts
Normal file
2
src/discounts/components/VoucherTypes/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./VoucherTypes";
|
||||
export * from "./VoucherTypes";
|
112
src/discounts/components/VoucherValue/VoucherValue.tsx
Normal file
112
src/discounts/components/VoucherValue/VoucherValue.tsx
Normal 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;
|
2
src/discounts/components/VoucherValue/index.ts
Normal file
2
src/discounts/components/VoucherValue/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./VoucherValue";
|
||||
export * from "./VoucherValue";
|
5
src/discounts/types.ts
Normal file
5
src/discounts/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export enum RequirementsPicker {
|
||||
ORDER = "ORDER",
|
||||
ITEM = "ITEM",
|
||||
NONE = "NONE"
|
||||
}
|
34
src/hooks/useElementScroll.ts
Normal file
34
src/hooks/useElementScroll.ts
Normal 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
148
src/hooks/useForm.ts
Normal 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
51
src/hooks/useFormset.ts
Normal 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;
|
47
src/hooks/useListActions.ts
Normal file
47
src/hooks/useListActions.ts
Normal 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;
|
30
src/hooks/useListSettings.ts
Normal file
30
src/hooks/useListSettings.ts
Normal 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
|
||||
};
|
||||
}
|
21
src/hooks/useLocalStorage.ts
Normal file
21
src/hooks/useLocalStorage.ts
Normal 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];
|
||||
}
|
2
src/hooks/useModalDialogErrors/index.ts
Normal file
2
src/hooks/useModalDialogErrors/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./useModalDialogErrors";
|
||||
export * from "./useModalDialogErrors";
|
44
src/hooks/useModalDialogErrors/useModalDialogErrors.test.ts
Normal file
44
src/hooks/useModalDialogErrors/useModalDialogErrors.test.ts
Normal 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);
|
||||
});
|
17
src/hooks/useModalDialogErrors/useModalDialogErrors.ts
Normal file
17
src/hooks/useModalDialogErrors/useModalDialogErrors.ts
Normal 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;
|
2
src/hooks/useModalDialogOpen/index.ts
Normal file
2
src/hooks/useModalDialogOpen/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./useModalDialogOpen";
|
||||
export * from "./useModalDialogOpen";
|
55
src/hooks/useModalDialogOpen/useModalDialogOpen.test.ts
Normal file
55
src/hooks/useModalDialogOpen/useModalDialogOpen.test.ts
Normal 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);
|
||||
});
|
26
src/hooks/useModalDialogOpen/useModalDialogOpen.ts
Normal file
26
src/hooks/useModalDialogOpen/useModalDialogOpen.ts
Normal 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;
|
28
src/hooks/useMultiAutocomplete.ts
Normal file
28
src/hooks/useMultiAutocomplete.ts
Normal 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;
|
29
src/hooks/useSearchQuery.ts
Normal file
29
src/hooks/useSearchQuery.ts
Normal 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;
|
35
src/hooks/useStateFromProps.ts
Normal file
35
src/hooks/useStateFromProps.ts
Normal 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;
|
24
src/hooks/useWindowScroll.ts
Normal file
24
src/hooks/useWindowScroll.ts
Normal 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;
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './AssignAttributeDialog';
|
||||
export * from './AssignAttributeDialog';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './ProductTypeAttributeUnassignDialog';
|
||||
export * from './ProductTypeAttributeUnassignDialog';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './ProductTypeBulkAttributeUnassignDialog';
|
||||
export * from './ProductTypeBulkAttributeUnassignDialog';
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './ProductTypeDeleteDialog';
|
||||
export * from './ProductTypeDeleteDialog';
|
42
src/productTypes/containers/SearchAttributes/index.tsx
Normal file
42
src/productTypes/containers/SearchAttributes/index.tsx
Normal 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
|
||||
);
|
|
@ -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;
|
||||
}
|
74
src/productTypes/types/AssignAttribute.ts
Normal file
74
src/productTypes/types/AssignAttribute.ts
Normal 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[];
|
||||
}
|
75
src/productTypes/types/ProductTypeAttributeReorder.ts
Normal file
75
src/productTypes/types/ProductTypeAttributeReorder.ts
Normal 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;
|
||||
}
|
72
src/productTypes/types/UnassignAttribute.ts
Normal file
72
src/productTypes/types/UnassignAttribute.ts
Normal 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)[];
|
||||
}
|
214
src/products/components/ProductAttributes/ProductAttributes.tsx
Normal file
214
src/products/components/ProductAttributes/ProductAttributes.tsx
Normal 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;
|
2
src/products/components/ProductAttributes/index.ts
Normal file
2
src/products/components/ProductAttributes/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from './ProductAttributes';
|
||||
export * from './ProductAttributes';
|
133
src/products/components/ProductListPage/ProductListPage.tsx
Normal file
133
src/products/components/ProductListPage/ProductListPage.tsx
Normal 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;
|
2
src/products/components/ProductListPage/index.ts
Normal file
2
src/products/components/ProductListPage/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./ProductListPage";
|
||||
export * from "./ProductListPage";
|
224
src/products/utils/data.ts
Normal file
224
src/products/utils/data.ts
Normal 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
Loading…
Reference in a new issue