Allow all attributes to appear in grid in PLP (#1933)

* Remove unnecessary attribute filtering

* Enable all attribute types to be displayed in plp

* Improve attribute rendering in plp

* Remove obsolete filters

* Add story

* Rmove dashboard settings section

* Update snapshots

* Remove unused import

* Add column search

* Fix type

* Update messages

* Allow popper to appear on top of select

* Update snapshots

* Update label

* Use autocomplete from macaw

* Fix stories

* Remove unused imports

* Update macaw

* Update message

* Update messages and snapshots
This commit is contained in:
Dominik Żegleń 2022-04-28 10:43:05 +02:00 committed by GitHub
parent bd15c52ee5
commit 8fc48eb5f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 4437 additions and 3202 deletions

File diff suppressed because it is too large Load diff

View file

@ -1042,14 +1042,6 @@
"src_dot_attributes_dot_components_dot_AttributeListPage_dot_3916653510": { "src_dot_attributes_dot_components_dot_AttributeListPage_dot_3916653510": {
"string": "Search Attribute" "string": "Search Attribute"
}, },
"src_dot_attributes_dot_components_dot_AttributeListPage_dot_availableInGrid": {
"context": "attribute can be column in product list table",
"string": "Can be used as column"
},
"src_dot_attributes_dot_components_dot_AttributeListPage_dot_filterableInDashboard": {
"context": "use attribute in filtering",
"string": "Filterable in Dashboard"
},
"src_dot_attributes_dot_components_dot_AttributeListPage_dot_filterableInStorefront": { "src_dot_attributes_dot_components_dot_AttributeListPage_dot_filterableInStorefront": {
"context": "use attribute in filtering", "context": "use attribute in filtering",
"string": "Filterable in Storefront" "string": "Filterable in Storefront"
@ -1069,9 +1061,9 @@
"src_dot_attributes_dot_components_dot_AttributeList_dot_1192828581": { "src_dot_attributes_dot_components_dot_AttributeList_dot_1192828581": {
"string": "No attributes found" "string": "No attributes found"
}, },
"src_dot_attributes_dot_components_dot_AttributeList_dot_2186555805": { "src_dot_attributes_dot_components_dot_AttributeList_dot_1708933569": {
"context": "attribute can be searched in storefront", "context": "attribute can be searched in storefront",
"string": "Use in faceted search" "string": "Use as filter"
}, },
"src_dot_attributes_dot_components_dot_AttributeList_dot_2235596452": { "src_dot_attributes_dot_components_dot_AttributeList_dot_2235596452": {
"context": "attribute can be searched in dashboard", "context": "attribute can be searched in dashboard",
@ -1132,7 +1124,7 @@
}, },
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_filterableInStorefront": { "src_dot_attributes_dot_components_dot_AttributeProperties_dot_filterableInStorefront": {
"context": "attribute is filterable in storefront", "context": "attribute is filterable in storefront",
"string": "Use in Faceted Navigation" "string": "Use as filter"
}, },
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_storefrontPropertiesTitle": { "src_dot_attributes_dot_components_dot_AttributeProperties_dot_storefrontPropertiesTitle": {
"context": "attribute properties regarding storefront", "context": "attribute properties regarding storefront",
@ -2102,17 +2094,17 @@
"context": "Status label when object is unpublished in a channel", "context": "Status label when object is unpublished in a channel",
"string": "Unpublished" "string": "Unpublished"
}, },
"src_dot_components_dot_ColumnPicker_dot_1483881697": { "src_dot_components_dot_ColumnPicker_dot_columnLabel": {
"context": "button", "context": "input label",
"string": "Reset" "string": "Selected columns"
}, },
"src_dot_components_dot_ColumnPicker_dot_2539195044": { "src_dot_components_dot_ColumnPicker_dot_columnSubheader": {
"context": "select visible columns button", "context": "header",
"string": "Columns" "string": "Column settings"
}, },
"src_dot_components_dot_ColumnPicker_dot_2715399461": { "src_dot_components_dot_ColumnPicker_dot_title": {
"context": "pick columns to display", "context": "header",
"string": "{numberOfSelected} columns selected out of {numberOfTotal}" "string": "Customize list"
}, },
"src_dot_components_dot_CompanyAddressInput_dot_1139500589": { "src_dot_components_dot_CompanyAddressInput_dot_1139500589": {
"string": "Country" "string": "Country"
@ -7115,6 +7107,10 @@
"src_dot_requiredField": { "src_dot_requiredField": {
"string": "This field is required" "string": "This field is required"
}, },
"src_dot_reset": {
"context": "button",
"string": "Reset"
},
"src_dot_returned": { "src_dot_returned": {
"context": "order status", "context": "order status",
"string": "Returned" "string": "Returned"

7
package-lock.json generated
View file

@ -4828,11 +4828,12 @@
} }
}, },
"@saleor/macaw-ui": { "@saleor/macaw-ui": {
"version": "0.4.0", "version": "0.5.0",
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.4.0.tgz", "resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.5.0.tgz",
"integrity": "sha512-N0DUNByS72juCmoCjUO5CChp53VxsZGz99rpYsdID06LMZndlu/ZoBmKsJv1mZcqUCB7BIqh9hntNd6MDPHpCA==", "integrity": "sha512-1dTgbmBHplWpqqyX7kZZSVMz2FESNeUerD5AXPfRYpE6Cr0L+oEfY4NFZlhe2dTY8hPeLz+hPv36JLlD/sGWyA==",
"requires": { "requires": {
"clsx": "^1.1.1", "clsx": "^1.1.1",
"downshift": "^6.1.7",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"react-inlinesvg": "^2.3.0" "react-inlinesvg": "^2.3.0"

View file

@ -28,7 +28,7 @@
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.58", "@material-ui/lab": "^4.0.0-alpha.58",
"@material-ui/styles": "^4.11.4", "@material-ui/styles": "^4.11.4",
"@saleor/macaw-ui": "^0.4.0", "@saleor/macaw-ui": "^0.5.0",
"@saleor/sdk": "^0.4.4", "@saleor/sdk": "^0.4.4",
"@sentry/react": "^6.0.0", "@sentry/react": "^6.0.0",
"@types/faker": "^5.1.6", "@types/faker": "^5.1.6",

File diff suppressed because it is too large Load diff

View file

@ -158,7 +158,7 @@ const AttributeList: React.FC<AttributeListProps> = ({
onClick={() => onSort(AttributeListUrlSortField.useInFacetedSearch)} onClick={() => onSort(AttributeListUrlSortField.useInFacetedSearch)}
> >
<FormattedMessage <FormattedMessage
defaultMessage="Use in faceted search" defaultMessage="Use as filter"
description="attribute can be searched in storefront" description="attribute can be searched in storefront"
/> />
</TableCellHeader> </TableCellHeader>

View file

@ -5,8 +5,6 @@ import { createBooleanField } from "@saleor/utils/filters/fields";
import { defineMessages, IntlShape } from "react-intl"; import { defineMessages, IntlShape } from "react-intl";
export enum AttributeFilterKeys { export enum AttributeFilterKeys {
availableInGrid = "availableInGrid",
filterableInDashboard = "filterableInDashboard",
filterableInStorefront = "filterableInStorefront", filterableInStorefront = "filterableInStorefront",
isVariantOnly = "isVariantOnly", isVariantOnly = "isVariantOnly",
valueRequired = "valueRequired", valueRequired = "valueRequired",
@ -14,8 +12,6 @@ export enum AttributeFilterKeys {
} }
export interface AttributeListFilterOpts { export interface AttributeListFilterOpts {
availableInGrid: FilterOpts<boolean>;
filterableInDashboard: FilterOpts<boolean>;
filterableInStorefront: FilterOpts<boolean>; filterableInStorefront: FilterOpts<boolean>;
isVariantOnly: FilterOpts<boolean>; isVariantOnly: FilterOpts<boolean>;
valueRequired: FilterOpts<boolean>; valueRequired: FilterOpts<boolean>;
@ -23,14 +19,6 @@ export interface AttributeListFilterOpts {
} }
const messages = defineMessages({ const messages = defineMessages({
availableInGrid: {
defaultMessage: "Can be used as column",
description: "attribute can be column in product list table"
},
filterableInDashboard: {
defaultMessage: "Filterable in Dashboard",
description: "use attribute in filtering"
},
filterableInStorefront: { filterableInStorefront: {
defaultMessage: "Filterable in Storefront", defaultMessage: "Filterable in Storefront",
description: "use attribute in filtering" description: "use attribute in filtering"
@ -54,30 +42,6 @@ export function createFilterStructure(
opts: AttributeListFilterOpts opts: AttributeListFilterOpts
): IFilter<AttributeFilterKeys> { ): IFilter<AttributeFilterKeys> {
return [ return [
{
...createBooleanField(
AttributeFilterKeys.availableInGrid,
intl.formatMessage(messages.availableInGrid),
opts.availableInGrid.value,
{
negative: intl.formatMessage(commonMessages.no),
positive: intl.formatMessage(commonMessages.yes)
}
),
active: opts.availableInGrid.active
},
{
...createBooleanField(
AttributeFilterKeys.filterableInDashboard,
intl.formatMessage(messages.filterableInDashboard),
opts.filterableInDashboard.value,
{
negative: intl.formatMessage(commonMessages.no),
positive: intl.formatMessage(commonMessages.yes)
}
),
active: opts.filterableInDashboard.active
},
{ {
...createBooleanField( ...createBooleanField(
AttributeFilterKeys.filterableInStorefront, AttributeFilterKeys.filterableInStorefront,

View file

@ -1,11 +1,9 @@
import { Card, CardContent, TextField, Typography } from "@material-ui/core"; import { Card, CardContent, TextField, Typography } from "@material-ui/core";
import { ATTRIBUTE_TYPES_WITH_CONFIGURABLE_FACED_NAVIGATION } from "@saleor/attributes/utils/data"; import { ATTRIBUTE_TYPES_WITH_CONFIGURABLE_FACED_NAVIGATION } from "@saleor/attributes/utils/data";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
import ControlledSwitch from "@saleor/components/ControlledSwitch"; import ControlledSwitch from "@saleor/components/ControlledSwitch";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import { AttributeErrorFragment, AttributeTypeEnum } from "@saleor/graphql"; import { AttributeErrorFragment, AttributeTypeEnum } from "@saleor/graphql";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { getFormErrors } from "@saleor/utils/errors"; import { getFormErrors } from "@saleor/utils/errors";
@ -39,7 +37,7 @@ const messages = defineMessages({
description: "caption" description: "caption"
}, },
filterableInStorefront: { filterableInStorefront: {
defaultMessage: "Use in Faceted Navigation", defaultMessage: "Use as filter",
description: "attribute is filterable in storefront" description: "attribute is filterable in storefront"
}, },
storefrontPropertiesTitle: { storefrontPropertiesTitle: {
@ -77,10 +75,6 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
const formErrors = getFormErrors(["storefrontSearchPosition"], errors); const formErrors = getFormErrors(["storefrontSearchPosition"], errors);
const dashboardProperties = ATTRIBUTE_TYPES_WITH_CONFIGURABLE_FACED_NAVIGATION.includes(
data.inputType
);
const storefrontFacetedNavigationProperties = const storefrontFacetedNavigationProperties =
ATTRIBUTE_TYPES_WITH_CONFIGURABLE_FACED_NAVIGATION.includes( ATTRIBUTE_TYPES_WITH_CONFIGURABLE_FACED_NAVIGATION.includes(
data.inputType data.inputType
@ -90,41 +84,6 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
<Card> <Card>
<CardTitle title={intl.formatMessage(commonMessages.properties)} /> <CardTitle title={intl.formatMessage(commonMessages.properties)} />
<CardContent> <CardContent>
{/* <Typography variant="subtitle1">
<FormattedMessage
defaultMessage="General Properties"
description="attribute general properties section"
/>
</Typography>
<Hr />
<CardSpacer />
<ControlledSwitch
name={"" as keyof AttributePageFormData}
checked={false}
disabled={disabled}
label={
<>
<FormattedMessage
defaultMessage="Variant Attribute"
description="attribute is variant-only"
/>
<Typography variant="caption">
<FormattedMessage
defaultMessage="If enabled, you'll be able to use this attribute to create product variants"
/>
</Typography>
</>
}
onChange={onChange}
/> */}
<Typography variant="subtitle1">
<FormattedMessage {...messages.storefrontPropertiesTitle} />
</Typography>
<Hr />
{storefrontFacetedNavigationProperties && ( {storefrontFacetedNavigationProperties && (
<> <>
<ControlledCheckbox <ControlledCheckbox
@ -154,9 +113,9 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
/> />
</> </>
)} )}
<FormSpacer />
</> </>
)} )}
<FormSpacer />
<ControlledSwitch <ControlledSwitch
name={"visibleInStorefront" as keyof FormData} name={"visibleInStorefront" as keyof FormData}
label={ label={
@ -171,47 +130,6 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
onChange={onChange} onChange={onChange}
disabled={disabled} disabled={disabled}
/> />
{dashboardProperties && (
<>
<CardSpacer />
<Typography variant="subtitle1">
<FormattedMessage {...messages.dashboardPropertiesTitle} />
</Typography>
<Hr />
<CardSpacer />
<ControlledCheckbox
name={"filterableInDashboard" as keyof FormData}
label={
<>
<FormattedMessage {...messages.filterableInDashboard} />
<Typography variant="caption">
<FormattedMessage
{...messages.filterableInDashboardCaption}
/>
</Typography>
</>
}
checked={data.filterableInDashboard}
onChange={onChange}
disabled={disabled}
/>
<FormSpacer />
<ControlledCheckbox
name={"availableInGrid" as keyof FormData}
label={
<>
<FormattedMessage {...messages.availableInGrid} />
<Typography variant="caption">
<FormattedMessage {...messages.availableInGridCaption} />
</Typography>
</>
}
checked={data.availableInGrid}
onChange={onChange}
disabled={disabled}
/>
</>
)}
</CardContent> </CardContent>
</Card> </Card>
); );

View file

@ -15,8 +15,6 @@ import {
export const attributeSection = "/attributes/"; export const attributeSection = "/attributes/";
export enum AttributeListUrlFiltersEnum { export enum AttributeListUrlFiltersEnum {
availableInGrid = "availableInGrid",
filterableInDashboard = "filterableInDashboard",
filterableInStorefront = "filterableInStorefront", filterableInStorefront = "filterableInStorefront",
isVariantOnly = "isVariantOnly", isVariantOnly = "isVariantOnly",
valueRequired = "valueRequired", valueRequired = "valueRequired",

View file

@ -6,6 +6,7 @@ import {
import { import {
AttributeEntityTypeEnum, AttributeEntityTypeEnum,
AttributeErrorFragment, AttributeErrorFragment,
AttributeFragment,
AttributeInputTypeEnum, AttributeInputTypeEnum,
AttributeValueDeleteMutation, AttributeValueDeleteMutation,
AttributeValueFragment, AttributeValueFragment,
@ -51,6 +52,14 @@ export const ATTRIBUTE_TYPES_WITH_CONFIGURABLE_FACED_NAVIGATION = [
AttributeInputTypeEnum.SWATCH AttributeInputTypeEnum.SWATCH
]; ];
export function filterable(
attribute: Pick<AttributeFragment, "inputType">
): boolean {
return ATTRIBUTE_TYPES_WITH_CONFIGURABLE_FACED_NAVIGATION.includes(
attribute.inputType
);
}
export interface AttributeReference { export interface AttributeReference {
label: string; label: string;
value: string; value: string;

View file

@ -2,8 +2,6 @@
exports[`Filtering URL params should not be empty if active filters are present 1`] = ` exports[`Filtering URL params should not be empty if active filters are present 1`] = `
Object { Object {
"availableInGrid": "true",
"filterableInDashboard": "true",
"filterableInStorefront": "true", "filterableInStorefront": "true",
"isVariantOnly": "true", "isVariantOnly": "true",
"valueRequired": "true", "valueRequired": "true",
@ -11,4 +9,4 @@ Object {
} }
`; `;
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"availableInGrid=true&filterableInDashboard=true&filterableInStorefront=true&isVariantOnly=true&valueRequired=true&visibleInStorefront=true"`; exports[`Filtering URL params should not be empty if active filters are present 2`] = `"filterableInStorefront=true&isVariantOnly=true&valueRequired=true&visibleInStorefront=true"`;

View file

@ -18,7 +18,7 @@ describe("Filtering query params", () => {
it("should not be empty object if params given", () => { it("should not be empty object if params given", () => {
const params: AttributeListUrlFilters = { const params: AttributeListUrlFilters = {
availableInGrid: true.toString() isVariantOnly: true.toString()
}; };
const filterVariables = getFilterVariables(params); const filterVariables = getFilterVariables(params);
@ -30,14 +30,6 @@ describe("Filtering URL params", () => {
const intl = createIntl(config); const intl = createIntl(config);
const filters = createFilterStructure(intl, { const filters = createFilterStructure(intl, {
availableInGrid: {
active: false,
value: true
},
filterableInDashboard: {
active: false,
value: true
},
filterableInStorefront: { filterableInStorefront: {
active: false, active: false,
value: true value: true

View file

@ -23,14 +23,6 @@ export function getFilterOpts(
params: AttributeListUrlFilters params: AttributeListUrlFilters
): AttributeListFilterOpts { ): AttributeListFilterOpts {
return { return {
availableInGrid: {
active: params.availableInGrid !== undefined,
value: maybe(() => parseBoolean(params.availableInGrid, true))
},
filterableInDashboard: {
active: params.filterableInDashboard !== undefined,
value: maybe(() => parseBoolean(params.filterableInDashboard, true))
},
filterableInStorefront: { filterableInStorefront: {
active: params.filterableInStorefront !== undefined, active: params.filterableInStorefront !== undefined,
value: maybe(() => parseBoolean(params.filterableInStorefront, true)) value: maybe(() => parseBoolean(params.filterableInStorefront, true))
@ -54,14 +46,6 @@ export function getFilterVariables(
params: AttributeListUrlFilters params: AttributeListUrlFilters
): AttributeFilterInput { ): AttributeFilterInput {
return { return {
availableInGrid:
params.availableInGrid !== undefined
? parseBoolean(params.availableInGrid, false)
: undefined,
filterableInDashboard:
params.filterableInDashboard !== undefined
? parseBoolean(params.filterableInDashboard, false)
: undefined,
filterableInStorefront: filterableInStorefront:
params.filterableInStorefront !== undefined params.filterableInStorefront !== undefined
? parseBoolean(params.filterableInStorefront, false) ? parseBoolean(params.filterableInStorefront, false)
@ -88,18 +72,6 @@ export function getFilterQueryParam(
const { name } = filter; const { name } = filter;
switch (name) { switch (name) {
case AttributeFilterKeys.availableInGrid:
return getSingleValueQueryParam(
filter,
AttributeListUrlFiltersEnum.availableInGrid
);
case AttributeFilterKeys.filterableInDashboard:
return getSingleValueQueryParam(
filter,
AttributeListUrlFiltersEnum.filterableInDashboard
);
case AttributeFilterKeys.filterableInStorefront: case AttributeFilterKeys.filterableInStorefront:
return getSingleValueQueryParam( return getSingleValueQueryParam(
filter, filter,

View file

@ -1,31 +1,33 @@
import { ClickAwayListener, Grow, Popper } from "@material-ui/core"; import { ClickAwayListener, Grow, Popper } from "@material-ui/core";
import { FormChange } from "@saleor/hooks/useForm";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { makeStyles } from "@saleor/macaw-ui"; import { Choice, ColumnsIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
import { toggle } from "@saleor/utils/lists"; import { FetchMoreProps } from "@saleor/types";
import { score } from "fuzzaldrin";
import sortBy from "lodash/sortBy";
import React from "react"; import React from "react";
import ColumnPickerButton from "./ColumnPickerButton"; import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField";
import ColumnPickerContent, { import ColumnPickerContent, {
ColumnPickerContentProps ColumnPickerContentProps
} from "./ColumnPickerContent"; } from "./ColumnPickerContent";
export interface ColumnPickerProps export interface ColumnPickerProps
extends Omit< extends FetchMoreProps,
ColumnPickerContentProps, Pick<ColumnPickerContentProps, "onQueryChange"> {
"selectedColumns" | "onCancel" | "onColumnToggle" | "onReset" | "onSave"
> {
className?: string; className?: string;
availableColumns: MultiAutocompleteChoiceType[];
defaultColumns: string[]; defaultColumns: string[];
initialColumns: string[]; initialColumns: Choice[];
initialOpen?: boolean; initialOpen?: boolean;
query: string;
onSave: (columns: string[]) => void; onSave: (columns: string[]) => void;
} }
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => ({
popper: { popper: {
marginTop: theme.spacing(1), marginTop: theme.spacing(1)
zIndex: 2
} }
}), }),
{ {
@ -36,54 +38,77 @@ const useStyles = makeStyles(
const ColumnPicker: React.FC<ColumnPickerProps> = props => { const ColumnPicker: React.FC<ColumnPickerProps> = props => {
const { const {
className, className,
columns, availableColumns,
defaultColumns, defaultColumns,
hasMore,
initialColumns, initialColumns,
initialOpen = false, initialOpen = false,
total, onSave,
onFetchMore, query,
onSave ...rest
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
const anchor = React.useRef<HTMLDivElement>(); const anchor = React.useRef<HTMLDivElement>();
const selectedColumns = React.useRef(
initialColumns.map(({ value }) => value)
);
const [isExpanded, setExpansionState] = React.useState(false); const [isExpanded, setExpansionState] = React.useState(false);
const [selectedColumns, setSelectedColumns] = useStateFromProps(
// Component is uncontrolled but we need to reset it somehow, so we change
// initial prop after reset callback to force value refreshing
const [initialColumnsChoices, setInitialColumnsChoices] = useStateFromProps(
initialColumns initialColumns
); );
const onChange: FormChange<string[]> = event => {
selectedColumns.current = event.target.value;
};
React.useEffect(() => { React.useEffect(() => {
setTimeout(() => setExpansionState(initialOpen), 100); setTimeout(() => setExpansionState(initialOpen), 100);
}, []); }, []);
const handleCancel = () => { const handleCancel = () => {
setExpansionState(false); setExpansionState(false);
setSelectedColumns(initialColumns); selectedColumns.current = initialColumns.map(({ value }) => value);
}; };
const handleColumnToggle = (column: string) => const handleReset = () => {
setSelectedColumns(toggle(column, selectedColumns, (a, b) => a === b)); selectedColumns.current = defaultColumns;
const defaultColumnsChoices = defaultColumns.map(value => ({
const handleReset = () => setSelectedColumns(defaultColumns); label: availableColumns.find(column => column.value === value)?.label,
value
}));
setInitialColumnsChoices(defaultColumnsChoices);
onChange({ target: { name: "", value: defaultColumns } });
};
const handleSave = () => { const handleSave = () => {
setExpansionState(false); setExpansionState(false);
onSave(selectedColumns); onSave(selectedColumns.current);
}; };
const choices = sortBy(
availableColumns.map(column => ({
...column,
score: -score(column.label, query)
})),
"score"
);
return ( return (
<ClickAwayListener onClickAway={() => setExpansionState(false)}> <ClickAwayListener onClickAway={() => setExpansionState(false)}>
<div ref={anchor} className={className}> <div ref={anchor} className={className}>
<ColumnPickerButton <IconButton
active={isExpanded} state={isExpanded ? "active" : "default"}
onClick={() => setExpansionState(prevState => !prevState)} onClick={() => setExpansionState(prevState => !prevState)}
/> >
<ColumnsIcon />
</IconButton>
<Popper <Popper
className={classes.popper} className={classes.popper}
open={isExpanded} open={isExpanded}
anchorEl={anchor.current} anchorEl={anchor.current}
transition transition
disablePortal
placement="bottom-end" placement="bottom-end"
> >
{({ TransitionProps, placement }) => ( {({ TransitionProps, placement }) => (
@ -95,15 +120,13 @@ const ColumnPicker: React.FC<ColumnPickerProps> = props => {
}} }}
> >
<ColumnPickerContent <ColumnPickerContent
columns={columns} choices={choices}
hasMore={hasMore} initialValues={initialColumnsChoices}
selectedColumns={selectedColumns}
total={total}
onCancel={handleCancel} onCancel={handleCancel}
onColumnToggle={handleColumnToggle} onChange={onChange}
onFetchMore={onFetchMore}
onReset={handleReset} onReset={handleReset}
onSave={handleSave} onSave={handleSave}
{...rest}
/> />
</Grow> </Grow>
)} )}

View file

@ -1,63 +0,0 @@
import { fade } from "@material-ui/core/styles/colorManipulator";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import { Button, makeStyles } from "@saleor/macaw-ui";
import classNames from "classnames";
import React from "react";
import { FormattedMessage } from "react-intl";
interface ColumnPickerButtonProps {
active: boolean;
className?: string;
onClick: () => void;
}
const useStyles = makeStyles(
theme => ({
icon: {
marginLeft: theme.spacing(2),
transition: theme.transitions.duration.short + "ms"
},
root: {
"& span": {
color: theme.palette.primary.main
},
paddingRight: theme.spacing(1)
},
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
})}
onClick={onClick}
variant="secondary"
>
<FormattedMessage
defaultMessage="Columns"
description="select visible columns button"
/>
<ArrowDropDownIcon
className={classNames(classes.icon, {
[classes.rotate]: active
})}
/>
</Button>
);
};
export default ColumnPickerButton;

View file

@ -1,194 +1,123 @@
import { import {
Card, Card,
CardActions,
CardContent, CardContent,
CircularProgress, CardHeader,
MenuItem,
Typography Typography
} from "@material-ui/core"; } from "@material-ui/core";
import useElementScroll from "@saleor/hooks/useElementScroll"; import { FormChange } from "@saleor/hooks/useForm";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { Button, makeStyles } from "@saleor/macaw-ui"; import {
Button,
Choice,
CloseIcon,
IconButton,
makeStyles,
MultipleValueAutocomplete
} from "@saleor/macaw-ui";
import { FetchMoreProps } from "@saleor/types"; import { FetchMoreProps } from "@saleor/types";
import { isSelected } from "@saleor/utils/lists";
import classNames from "classnames";
import React from "react"; import React from "react";
import InfiniteScroll from "react-infinite-scroll-component"; import { FormattedMessage, useIntl } from "react-intl";
import { FormattedMessage } from "react-intl";
import ControlledCheckbox from "../ControlledCheckbox"; import messages from "./messages";
import Hr from "../Hr";
export interface ColumnPickerChoice { export interface ColumnPickerContentProps extends FetchMoreProps {
label: string; choices: Choice[];
value: string; initialValues: Choice[];
}
export interface ColumnPickerContentProps extends Partial<FetchMoreProps> {
columns: ColumnPickerChoice[];
selectedColumns: string[];
total?: number;
onCancel: () => void; onCancel: () => void;
onColumnToggle: (column: string) => void; onChange: FormChange<string[]>;
onReset: () => void; onReset: () => void;
onSave: () => void; onSave: () => void;
onQueryChange: (query: string) => void;
} }
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => ({
actionBar: { actions: {
display: "flex", flexDirection: "row-reverse",
justifyContent: "space-between" gap: theme.spacing(1),
}, paddingBottom: theme.spacing(2)
actionBarContainer: {
"&&": {
padding: theme.spacing(2)
},
boxShadow: `0px 0px 0px 0px ${theme.palette.background.paper}`,
transition: theme.transitions.duration.short + "ms"
},
cancelButton: {
marginRight: theme.spacing(2)
},
dialogPaper: {
overflow: "hidden"
}, },
content: { content: {
[theme.breakpoints.down("sm")]: { paddingBottom: theme.spacing(2),
gridTemplateColumns: "repeat(2, 1fr)" width: 450
},
display: "grid",
gridColumnGap: theme.spacing(3),
gridTemplateColumns: "repeat(3, 210px)",
padding: theme.spacing(2, 3)
}, },
contentContainer: { subHeader: {
maxHeight: 256, fontWeight: 500,
overflowX: "visible", letterSpacing: "0.1rem",
overflowY: "scroll", textTransform: "uppercase",
padding: 0 marginBottom: theme.spacing(1)
},
dropShadow: {
boxShadow: `0px -5px 10px 0px ${theme.palette.divider}`
},
label: {
"& span": {
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis"
},
marginRight: 0
},
loadMoreLoaderContainer: {
alignItems: "center",
display: "flex",
gridColumnEnd: "span 3",
height: theme.spacing(3),
justifyContent: "center"
},
titleContainer: {
padding: theme.spacing(1.5, 3.5)
} }
}), }),
{ name: "ColumnPickerContent" } { name: "ColumnPickerContent" }
); );
const scrollableTargetId = "columnPickerScrollableDiv";
const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => { const ColumnPickerContent: React.FC<ColumnPickerContentProps> = props => {
const { const {
columns, choices,
hasMore, initialValues,
selectedColumns, loading,
total,
onCancel, onCancel,
onColumnToggle, onChange,
onFetchMore,
onReset, onReset,
onSave onFetchMore,
onSave,
onQueryChange
} = props; } = props;
const classes = useStyles(props); const classes = useStyles();
const anchor = React.useRef<HTMLDivElement>(); const intl = useIntl();
const scrollPosition = useElementScroll(anchor);
const dropShadow =
anchor.current && scrollPosition
? scrollPosition.y + anchor.current.clientHeight <
anchor.current.scrollHeight
: false;
return ( return (
<Card elevation={8}> <Card elevation={8}>
<CardContent className={classes.titleContainer}> <CardHeader
<Typography color="textSecondary"> action={
<FormattedMessage <IconButton variant="secondary" onClick={onCancel}>
defaultMessage="{numberOfSelected} columns selected out of {numberOfTotal}" <CloseIcon />
description="pick columns to display" </IconButton>
values={{ }
numberOfSelected: selectedColumns.length, title={intl.formatMessage(messages.title)}
numberOfTotal: total || columns.length />
}} <CardContent className={classes.content}>
/> <Typography
</Typography> color="textSecondary"
</CardContent> variant="caption"
<Hr /> className={classes.subHeader}
<CardContent
className={classes.contentContainer}
ref={anchor}
id={scrollableTargetId}
>
<InfiniteScroll
dataLength={columns.length}
next={onFetchMore}
hasMore={hasMore}
scrollThreshold="100px"
loader={
<div className={classes.loadMoreLoaderContainer}>
<CircularProgress size={16} />
</div>
}
scrollableTarget={scrollableTargetId}
> >
<div className={classes.content}> {intl.formatMessage(messages.columnSubheader)}
{columns.map(column => ( </Typography>
<ControlledCheckbox <MultipleValueAutocomplete
className={classes.label} choices={choices}
checked={isSelected( enableReinitialize
column.value, fullWidth
selectedColumns, label={intl.formatMessage(messages.columnLabel)}
(a, b) => a === b loading={loading}
)} name="columns"
name={column.value} initialValue={initialValues}
label={column.label} onChange={onChange}
onChange={() => onColumnToggle(column.value)} onInputChange={onQueryChange}
key={column.value} onScrollToBottom={onFetchMore}
/> >
))} {({ choices, getItemProps }) =>
</div> choices.map((choice, choiceIndex) => (
</InfiniteScroll> <MenuItem
</CardContent> key={choice.value}
<Hr /> {...getItemProps({ item: choice, index: choiceIndex })}
<CardContent >
className={classNames(classes.actionBarContainer, { {choice.label}
[classes.dropShadow]: dropShadow </MenuItem>
})} ))
> }
<div className={classes.actionBar}> </MultipleValueAutocomplete>
<Button variant="secondary" color="text" onClick={onReset}>
<FormattedMessage defaultMessage="Reset" description="button" />
</Button>
<div>
<Button
className={classes.cancelButton}
color="text"
variant="secondary"
onClick={onCancel}
>
<FormattedMessage {...buttonMessages.cancel} />
</Button>
<Button variant="primary" onClick={onSave}>
<FormattedMessage {...buttonMessages.save} />
</Button>
</div>
</div>
</CardContent> </CardContent>
<CardActions className={classes.actions}>
<Button variant="primary" onClick={onSave}>
<FormattedMessage {...buttonMessages.save} />
</Button>
<Button color="text" variant="secondary" onClick={onReset}>
<FormattedMessage {...buttonMessages.reset} />
</Button>
</CardActions>
</Card> </Card>
); );
}; };

View file

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

View file

@ -0,0 +1,18 @@
import { defineMessages } from "react-intl";
const messages = defineMessages({
title: {
defaultMessage: "Customize list",
description: "header"
},
columnSubheader: {
defaultMessage: "Column settings",
description: "header"
},
columnLabel: {
defaultMessage: "Selected columns",
description: "input label"
}
});
export default messages;

View file

@ -1,4 +1,9 @@
import { Popper, TextField, Typography } from "@material-ui/core"; import {
Popper,
PopperPlacementType,
TextField,
Typography
} from "@material-ui/core";
import { fade } from "@material-ui/core/styles/colorManipulator"; import { fade } from "@material-ui/core/styles/colorManipulator";
import CloseIcon from "@material-ui/icons/Close"; import CloseIcon from "@material-ui/icons/Close";
import Debounce, { DebounceProps } from "@saleor/components/Debounce"; import Debounce, { DebounceProps } from "@saleor/components/Debounce";
@ -103,6 +108,7 @@ export interface MultiAutocompleteSelectFieldProps
onBlur?: () => void; onBlur?: () => void;
fetchOnFocus?: boolean; fetchOnFocus?: boolean;
endAdornment?: React.ReactNode; endAdornment?: React.ReactNode;
popperPlacement?: PopperPlacementType;
} }
const DebounceAutocomplete: React.ComponentType<DebounceProps< const DebounceAutocomplete: React.ComponentType<DebounceProps<
@ -131,6 +137,7 @@ const MultiAutocompleteSelectFieldComponent: React.FC<MultiAutocompleteSelectFie
onFetchMore, onFetchMore,
fetchOnFocus, fetchOnFocus,
endAdornment, endAdornment,
popperPlacement = "bottom-end",
...rest ...rest
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
@ -229,7 +236,7 @@ const MultiAutocompleteSelectFieldComponent: React.FC<MultiAutocompleteSelectFie
width: anchor.current.clientWidth, width: anchor.current.clientWidth,
zIndex: 1301 zIndex: 1301
}} }}
placement="bottom-end" placement={popperPlacement}
> >
<MultiAutocompleteSelectFieldContent <MultiAutocompleteSelectFieldContent
add={ add={

View file

@ -1,4 +1,9 @@
import { InputBase, Popper, TextField } from "@material-ui/core"; import {
InputBase,
Popper,
PopperPlacementType,
TextField
} from "@material-ui/core";
import { InputProps } from "@material-ui/core/Input"; import { InputProps } from "@material-ui/core/Input";
import { ExtendedFormHelperTextProps } from "@saleor/channels/components/ChannelForm/types"; import { ExtendedFormHelperTextProps } from "@saleor/channels/components/ChannelForm/types";
import { ChevronIcon, makeStyles } from "@saleor/macaw-ui"; import { ChevronIcon, makeStyles } from "@saleor/macaw-ui";
@ -62,6 +67,7 @@ export interface SingleAutocompleteSelectFieldProps
FormHelperTextProps?: ExtendedFormHelperTextProps; FormHelperTextProps?: ExtendedFormHelperTextProps;
nakedInput?: boolean; nakedInput?: boolean;
onBlur?: () => void; onBlur?: () => void;
popperPlacement?: PopperPlacementType;
} }
const DebounceAutocomplete: React.ComponentType<DebounceProps< const DebounceAutocomplete: React.ComponentType<DebounceProps<
@ -93,6 +99,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
FormHelperTextProps, FormHelperTextProps,
nakedInput = false, nakedInput = false,
onBlur, onBlur,
popperPlacement = "bottom-end",
...rest ...rest
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
@ -250,7 +257,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
anchorEl={anchor.current} anchorEl={anchor.current}
open={isOpen} open={isOpen}
style={{ width: anchor.current.clientWidth, zIndex: 1301 }} style={{ width: anchor.current.clientWidth, zIndex: 1301 }}
placement="bottom-end" placement={popperPlacement}
> >
<SingleAutocompleteSelectFieldContent <SingleAutocompleteSelectFieldContent
add={ add={

View file

@ -357,3 +357,14 @@ export const exportFileFragment = gql`
url url
} }
`; `;
export const productListAttribute = gql`
fragment ProductListAttribute on SelectedAttribute {
attribute {
id
}
values {
...AttributeValue
}
}
`;

View file

@ -2043,6 +2043,16 @@ export const ExportFileFragmentDoc = gql`
url url
} }
`; `;
export const ProductListAttributeFragmentDoc = gql`
fragment ProductListAttribute on SelectedAttribute {
attribute {
id
}
values {
...AttributeValue
}
}
${AttributeValueFragmentDoc}`;
export const ShippingMethodWithPostalCodesFragmentDoc = gql` export const ShippingMethodWithPostalCodesFragmentDoc = gql`
fragment ShippingMethodWithPostalCodes on ShippingMethodType { fragment ShippingMethodWithPostalCodes on ShippingMethodType {
id id
@ -11931,10 +11941,7 @@ export type ProductVariantPreorderDeactivateMutationResult = Apollo.MutationResu
export type ProductVariantPreorderDeactivateMutationOptions = Apollo.BaseMutationOptions<Types.ProductVariantPreorderDeactivateMutation, Types.ProductVariantPreorderDeactivateMutationVariables>; export type ProductVariantPreorderDeactivateMutationOptions = Apollo.BaseMutationOptions<Types.ProductVariantPreorderDeactivateMutation, Types.ProductVariantPreorderDeactivateMutationVariables>;
export const InitialProductFilterAttributesDocument = gql` export const InitialProductFilterAttributesDocument = gql`
query InitialProductFilterAttributes { query InitialProductFilterAttributes {
attributes( attributes(first: 100, filter: {type: PRODUCT_TYPE}) {
first: 100
filter: {filterableInDashboard: true, type: PRODUCT_TYPE}
) {
edges { edges {
node { node {
id id
@ -12109,12 +12116,7 @@ export const ProductListDocument = gql`
...ProductWithChannelListings ...ProductWithChannelListings
updatedAt updatedAt
attributes @include(if: $hasSelectedAttributes) { attributes @include(if: $hasSelectedAttributes) {
attribute { ...ProductListAttribute
id
}
values {
...AttributeValue
}
} }
} }
} }
@ -12128,7 +12130,7 @@ export const ProductListDocument = gql`
} }
} }
${ProductWithChannelListingsFragmentDoc} ${ProductWithChannelListingsFragmentDoc}
${AttributeValueFragmentDoc}`; ${ProductListAttributeFragmentDoc}`;
/** /**
* __useProductListQuery__ * __useProductListQuery__
@ -12475,55 +12477,6 @@ export function useProductMediaByIdLazyQuery(baseOptions?: ApolloReactHooks.Lazy
export type ProductMediaByIdQueryHookResult = ReturnType<typeof useProductMediaByIdQuery>; export type ProductMediaByIdQueryHookResult = ReturnType<typeof useProductMediaByIdQuery>;
export type ProductMediaByIdLazyQueryHookResult = ReturnType<typeof useProductMediaByIdLazyQuery>; export type ProductMediaByIdLazyQueryHookResult = ReturnType<typeof useProductMediaByIdLazyQuery>;
export type ProductMediaByIdQueryResult = Apollo.QueryResult<Types.ProductMediaByIdQuery, Types.ProductMediaByIdQueryVariables>; export type ProductMediaByIdQueryResult = Apollo.QueryResult<Types.ProductMediaByIdQuery, Types.ProductMediaByIdQueryVariables>;
export const AvailableInGridAttributesDocument = gql`
query AvailableInGridAttributes($first: Int!, $after: String) {
availableInGrid: attributes(
first: $first
after: $after
filter: {availableInGrid: true, isVariantOnly: false, type: PRODUCT_TYPE}
) {
edges {
node {
id
name
}
}
pageInfo {
...PageInfo
}
totalCount
}
}
${PageInfoFragmentDoc}`;
/**
* __useAvailableInGridAttributesQuery__
*
* To run a query within a React component, call `useAvailableInGridAttributesQuery` and pass it any options that fit your needs.
* When your component renders, `useAvailableInGridAttributesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useAvailableInGridAttributesQuery({
* variables: {
* first: // value for 'first'
* after: // value for 'after'
* },
* });
*/
export function useAvailableInGridAttributesQuery(baseOptions: ApolloReactHooks.QueryHookOptions<Types.AvailableInGridAttributesQuery, Types.AvailableInGridAttributesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useQuery<Types.AvailableInGridAttributesQuery, Types.AvailableInGridAttributesQueryVariables>(AvailableInGridAttributesDocument, options);
}
export function useAvailableInGridAttributesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<Types.AvailableInGridAttributesQuery, Types.AvailableInGridAttributesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useLazyQuery<Types.AvailableInGridAttributesQuery, Types.AvailableInGridAttributesQueryVariables>(AvailableInGridAttributesDocument, options);
}
export type AvailableInGridAttributesQueryHookResult = ReturnType<typeof useAvailableInGridAttributesQuery>;
export type AvailableInGridAttributesLazyQueryHookResult = ReturnType<typeof useAvailableInGridAttributesLazyQuery>;
export type AvailableInGridAttributesQueryResult = Apollo.QueryResult<Types.AvailableInGridAttributesQuery, Types.AvailableInGridAttributesQueryVariables>;
export const GridAttributesDocument = gql` export const GridAttributesDocument = gql`
query GridAttributes($ids: [ID!]!) { query GridAttributes($ids: [ID!]!) {
grid: attributes(first: 25, filter: {ids: $ids}) { grid: attributes(first: 25, filter: {ids: $ids}) {
@ -12705,6 +12658,56 @@ export function useSearchAttributeValuesLazyQuery(baseOptions?: ApolloReactHooks
export type SearchAttributeValuesQueryHookResult = ReturnType<typeof useSearchAttributeValuesQuery>; export type SearchAttributeValuesQueryHookResult = ReturnType<typeof useSearchAttributeValuesQuery>;
export type SearchAttributeValuesLazyQueryHookResult = ReturnType<typeof useSearchAttributeValuesLazyQuery>; export type SearchAttributeValuesLazyQueryHookResult = ReturnType<typeof useSearchAttributeValuesLazyQuery>;
export type SearchAttributeValuesQueryResult = Apollo.QueryResult<Types.SearchAttributeValuesQuery, Types.SearchAttributeValuesQueryVariables>; export type SearchAttributeValuesQueryResult = Apollo.QueryResult<Types.SearchAttributeValuesQuery, Types.SearchAttributeValuesQueryVariables>;
export const SearchAvailableInGridAttributesDocument = gql`
query SearchAvailableInGridAttributes($first: Int!, $after: String, $query: String!) {
availableInGrid: attributes(
first: $first
after: $after
filter: {isVariantOnly: false, type: PRODUCT_TYPE, search: $query}
) {
edges {
node {
id
name
}
}
pageInfo {
...PageInfo
}
totalCount
}
}
${PageInfoFragmentDoc}`;
/**
* __useSearchAvailableInGridAttributesQuery__
*
* To run a query within a React component, call `useSearchAvailableInGridAttributesQuery` and pass it any options that fit your needs.
* When your component renders, `useSearchAvailableInGridAttributesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useSearchAvailableInGridAttributesQuery({
* variables: {
* first: // value for 'first'
* after: // value for 'after'
* query: // value for 'query'
* },
* });
*/
export function useSearchAvailableInGridAttributesQuery(baseOptions: ApolloReactHooks.QueryHookOptions<Types.SearchAvailableInGridAttributesQuery, Types.SearchAvailableInGridAttributesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useQuery<Types.SearchAvailableInGridAttributesQuery, Types.SearchAvailableInGridAttributesQueryVariables>(SearchAvailableInGridAttributesDocument, options);
}
export function useSearchAvailableInGridAttributesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<Types.SearchAvailableInGridAttributesQuery, Types.SearchAvailableInGridAttributesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return ApolloReactHooks.useLazyQuery<Types.SearchAvailableInGridAttributesQuery, Types.SearchAvailableInGridAttributesQueryVariables>(SearchAvailableInGridAttributesDocument, options);
}
export type SearchAvailableInGridAttributesQueryHookResult = ReturnType<typeof useSearchAvailableInGridAttributesQuery>;
export type SearchAvailableInGridAttributesLazyQueryHookResult = ReturnType<typeof useSearchAvailableInGridAttributesLazyQuery>;
export type SearchAvailableInGridAttributesQueryResult = Apollo.QueryResult<Types.SearchAvailableInGridAttributesQuery, Types.SearchAvailableInGridAttributesQueryVariables>;
export const SearchAvailablePageAttributesDocument = gql` export const SearchAvailablePageAttributesDocument = gql`
query SearchAvailablePageAttributes($id: ID!, $after: String, $first: Int!, $query: String!) { query SearchAvailablePageAttributes($id: ID!, $after: String, $first: Int!, $query: String!) {
pageType(id: $id) { pageType(id: $id) {

View file

@ -520,7 +520,11 @@ export type CatalogueInput = {
categories?: InputMaybe<Array<Scalars['ID']>>; categories?: InputMaybe<Array<Scalars['ID']>>;
/** Collections related to the discount. */ /** Collections related to the discount. */
collections?: InputMaybe<Array<Scalars['ID']>>; collections?: InputMaybe<Array<Scalars['ID']>>;
/** Added in Saleor 3.1. Product variant related to the discount. */ /**
* Product variant related to the discount.
*
* Added in Saleor 3.1.
*/
variants?: InputMaybe<Array<Scalars['ID']>>; variants?: InputMaybe<Array<Scalars['ID']>>;
}; };
@ -576,7 +580,11 @@ export type ChannelCreateInput = {
slug: Scalars['String']; slug: Scalars['String'];
/** Currency of the channel. */ /** Currency of the channel. */
currencyCode: Scalars['String']; currencyCode: Scalars['String'];
/** Added in Saleor 3.1. Default country for the channel. Default country can be used in checkout to determine the stock quantities or calculate taxes when the country was not explicitly provided. */ /**
* Default country for the channel. Default country can be used in checkout to determine the stock quantities or calculate taxes when the country was not explicitly provided.
*
* Added in Saleor 3.1.
*/
defaultCountry: CountryCode; defaultCountry: CountryCode;
/** List of shipping zones to assign to the channel. */ /** List of shipping zones to assign to the channel. */
addShippingZones?: InputMaybe<Array<Scalars['ID']>>; addShippingZones?: InputMaybe<Array<Scalars['ID']>>;
@ -607,7 +615,11 @@ export type ChannelUpdateInput = {
name?: InputMaybe<Scalars['String']>; name?: InputMaybe<Scalars['String']>;
/** Slug of the channel. */ /** Slug of the channel. */
slug?: InputMaybe<Scalars['String']>; slug?: InputMaybe<Scalars['String']>;
/** Added in Saleor 3.1. Default country for the channel. Default country can be used in checkout to determine the stock quantities or calculate taxes when the country was not explicitly provided. */ /**
* Default country for the channel. Default country can be used in checkout to determine the stock quantities or calculate taxes when the country was not explicitly provided.
*
* Added in Saleor 3.1.
*/
defaultCountry?: InputMaybe<CountryCode>; defaultCountry?: InputMaybe<CountryCode>;
/** List of shipping zones to assign to the channel. */ /** List of shipping zones to assign to the channel. */
addShippingZones?: InputMaybe<Array<Scalars['ID']>>; addShippingZones?: InputMaybe<Array<Scalars['ID']>>;
@ -674,7 +686,13 @@ export type CheckoutLineInput = {
quantity: Scalars['Int']; quantity: Scalars['Int'];
/** ID of the product variant. */ /** ID of the product variant. */
variantId: Scalars['ID']; variantId: Scalars['ID'];
/** Added in Saleor 3.1. Custom price of the item. Can be set only by apps with `HANDLE_CHECKOUTS` permission. When the line with the same variant will be provided multiple times, the last price will be used. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Custom price of the item. Can be set only by apps with `HANDLE_CHECKOUTS` permission. When the line with the same variant will be provided multiple times, the last price will be used.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
price?: InputMaybe<Scalars['PositiveDecimal']>; price?: InputMaybe<Scalars['PositiveDecimal']>;
}; };
@ -683,7 +701,13 @@ export type CheckoutLineUpdateInput = {
quantity?: InputMaybe<Scalars['Int']>; quantity?: InputMaybe<Scalars['Int']>;
/** ID of the product variant. */ /** ID of the product variant. */
variantId: Scalars['ID']; variantId: Scalars['ID'];
/** Added in Saleor 3.1. Custom price of the item. Can be set only by apps with `HANDLE_CHECKOUTS` permission. When the line with the same variant will be provided multiple times, the last price will be used. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Custom price of the item. Can be set only by apps with `HANDLE_CHECKOUTS` permission. When the line with the same variant will be provided multiple times, the last price will be used.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
price?: InputMaybe<Scalars['PositiveDecimal']>; price?: InputMaybe<Scalars['PositiveDecimal']>;
}; };
@ -1451,9 +1475,21 @@ export type GiftCardBulkCreateInput = {
}; };
export type GiftCardCreateInput = { export type GiftCardCreateInput = {
/** Added in Saleor 3.1. The gift card tags to add. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* The gift card tags to add.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
addTags?: InputMaybe<Array<Scalars['String']>>; addTags?: InputMaybe<Array<Scalars['String']>>;
/** Added in Saleor 3.1. The gift card expiry date. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* The gift card expiry date.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
expiryDate?: InputMaybe<Scalars['Date']>; expiryDate?: InputMaybe<Scalars['Date']>;
/** /**
* Start date of the gift card in ISO 8601 format. * Start date of the gift card in ISO 8601 format.
@ -1471,9 +1507,21 @@ export type GiftCardCreateInput = {
balance: PriceInput; balance: PriceInput;
/** Email of the customer to whom gift card will be sent. */ /** Email of the customer to whom gift card will be sent. */
userEmail?: InputMaybe<Scalars['String']>; userEmail?: InputMaybe<Scalars['String']>;
/** Added in Saleor 3.1. Slug of a channel from which the email should be sent. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Slug of a channel from which the email should be sent.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
channel?: InputMaybe<Scalars['String']>; channel?: InputMaybe<Scalars['String']>;
/** Added in Saleor 3.1. Determine if gift card is active. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Determine if gift card is active.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
isActive: Scalars['Boolean']; isActive: Scalars['Boolean'];
/** /**
* Code to use the gift card. * Code to use the gift card.
@ -1481,7 +1529,13 @@ export type GiftCardCreateInput = {
* DEPRECATED: this field will be removed in Saleor 4.0. The code is now auto generated. * DEPRECATED: this field will be removed in Saleor 4.0. The code is now auto generated.
*/ */
code?: InputMaybe<Scalars['String']>; code?: InputMaybe<Scalars['String']>;
/** Added in Saleor 3.1. The gift card note from the staff member. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* The gift card note from the staff member.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
note?: InputMaybe<Scalars['String']>; note?: InputMaybe<Scalars['String']>;
}; };
@ -1581,9 +1635,21 @@ export type GiftCardTagFilterInput = {
}; };
export type GiftCardUpdateInput = { export type GiftCardUpdateInput = {
/** Added in Saleor 3.1. The gift card tags to add. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* The gift card tags to add.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
addTags?: InputMaybe<Array<Scalars['String']>>; addTags?: InputMaybe<Array<Scalars['String']>>;
/** Added in Saleor 3.1. The gift card expiry date. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* The gift card expiry date.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
expiryDate?: InputMaybe<Scalars['Date']>; expiryDate?: InputMaybe<Scalars['Date']>;
/** /**
* Start date of the gift card in ISO 8601 format. * Start date of the gift card in ISO 8601 format.
@ -1597,9 +1663,21 @@ export type GiftCardUpdateInput = {
* DEPRECATED: this field will be removed in Saleor 4.0. Use `expiryDate` from `expirySettings` instead. * DEPRECATED: this field will be removed in Saleor 4.0. Use `expiryDate` from `expirySettings` instead.
*/ */
endDate?: InputMaybe<Scalars['Date']>; endDate?: InputMaybe<Scalars['Date']>;
/** Added in Saleor 3.1. The gift card tags to remove. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* The gift card tags to remove.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
removeTags?: InputMaybe<Array<Scalars['String']>>; removeTags?: InputMaybe<Array<Scalars['String']>>;
/** Added in Saleor 3.1. The gift card balance amount. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* The gift card balance amount.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
balanceAmount?: InputMaybe<Scalars['PositiveDecimal']>; balanceAmount?: InputMaybe<Scalars['PositiveDecimal']>;
}; };
@ -2976,7 +3054,11 @@ export type PageCreateInput = {
* DEPRECATED: this field will be removed in Saleor 4.0. Use `publishedAt` field instead. * DEPRECATED: this field will be removed in Saleor 4.0. Use `publishedAt` field instead.
*/ */
publicationDate?: InputMaybe<Scalars['String']>; publicationDate?: InputMaybe<Scalars['String']>;
/** Added in Saleor 3.3. Publication date time. ISO 8601 standard. */ /**
* Publication date time. ISO 8601 standard.
*
* Added in Saleor 3.3.
*/
publishedAt?: InputMaybe<Scalars['DateTime']>; publishedAt?: InputMaybe<Scalars['DateTime']>;
/** Search engine optimization fields. */ /** Search engine optimization fields. */
seo?: InputMaybe<SeoInput>; seo?: InputMaybe<SeoInput>;
@ -3019,7 +3101,11 @@ export type PageInput = {
* DEPRECATED: this field will be removed in Saleor 4.0. Use `publishedAt` field instead. * DEPRECATED: this field will be removed in Saleor 4.0. Use `publishedAt` field instead.
*/ */
publicationDate?: InputMaybe<Scalars['String']>; publicationDate?: InputMaybe<Scalars['String']>;
/** Added in Saleor 3.3. Publication date time. ISO 8601 standard. */ /**
* Publication date time. ISO 8601 standard.
*
* Added in Saleor 3.3.
*/
publishedAt?: InputMaybe<Scalars['DateTime']>; publishedAt?: InputMaybe<Scalars['DateTime']>;
/** Search engine optimization fields. */ /** Search engine optimization fields. */
seo?: InputMaybe<SeoInput>; seo?: InputMaybe<SeoInput>;
@ -3157,9 +3243,17 @@ export type PaymentInput = {
amount?: InputMaybe<Scalars['PositiveDecimal']>; amount?: InputMaybe<Scalars['PositiveDecimal']>;
/** URL of a storefront view where user should be redirected after requiring additional actions. Payment with additional actions will not be finished if this field is not provided. */ /** URL of a storefront view where user should be redirected after requiring additional actions. Payment with additional actions will not be finished if this field is not provided. */
returnUrl?: InputMaybe<Scalars['String']>; returnUrl?: InputMaybe<Scalars['String']>;
/** Added in Saleor 3.1. Payment store type. */ /**
* Payment store type.
*
* Added in Saleor 3.1.
*/
storePaymentMethod?: InputMaybe<StorePaymentMethodEnum>; storePaymentMethod?: InputMaybe<StorePaymentMethodEnum>;
/** Added in Saleor 3.1. User public metadata. */ /**
* User public metadata.
*
* Added in Saleor 3.1.
*/
metadata?: InputMaybe<Array<MetadataInput>>; metadata?: InputMaybe<Array<MetadataInput>>;
}; };
@ -3315,14 +3409,22 @@ export type ProductAttributeAssignInput = {
id: Scalars['ID']; id: Scalars['ID'];
/** The attribute type to be assigned as. */ /** The attribute type to be assigned as. */
type: ProductAttributeType; type: ProductAttributeType;
/** Added in Saleor 3.1. Whether attribute is allowed in variant selection. Allowed types are: ['dropdown', 'boolean', 'swatch', 'numeric']. */ /**
* Whether attribute is allowed in variant selection. Allowed types are: ['dropdown', 'boolean', 'swatch', 'numeric'].
*
* Added in Saleor 3.1.
*/
variantSelection?: InputMaybe<Scalars['Boolean']>; variantSelection?: InputMaybe<Scalars['Boolean']>;
}; };
export type ProductAttributeAssignmentUpdateInput = { export type ProductAttributeAssignmentUpdateInput = {
/** The ID of the attribute to assign. */ /** The ID of the attribute to assign. */
id: Scalars['ID']; id: Scalars['ID'];
/** Added in Saleor 3.1. Whether attribute is allowed in variant selection. Allowed types are: ['dropdown', 'boolean', 'swatch', 'numeric']. */ /**
* Whether attribute is allowed in variant selection. Allowed types are: ['dropdown', 'boolean', 'swatch', 'numeric'].
*
* Added in Saleor 3.1.
*/
variantSelection: Scalars['Boolean']; variantSelection: Scalars['Boolean'];
}; };
@ -3342,7 +3444,11 @@ export type ProductChannelListingAddInput = {
* DEPRECATED: this field will be removed in Saleor 4.0. Use `publishedAt` field instead. * DEPRECATED: this field will be removed in Saleor 4.0. Use `publishedAt` field instead.
*/ */
publicationDate?: InputMaybe<Scalars['Date']>; publicationDate?: InputMaybe<Scalars['Date']>;
/** Added in Saleor 3.3. Publication date time. ISO 8601 standard. */ /**
* Publication date time. ISO 8601 standard.
*
* Added in Saleor 3.3.
*/
publishedAt?: InputMaybe<Scalars['DateTime']>; publishedAt?: InputMaybe<Scalars['DateTime']>;
/** Determines if product is visible in product listings (doesn't apply to product collections). */ /** Determines if product is visible in product listings (doesn't apply to product collections). */
visibleInListings?: InputMaybe<Scalars['Boolean']>; visibleInListings?: InputMaybe<Scalars['Boolean']>;
@ -3354,7 +3460,11 @@ export type ProductChannelListingAddInput = {
* DEPRECATED: this field will be removed in Saleor 4.0. Use `availableForPurchaseAt` field instead. * DEPRECATED: this field will be removed in Saleor 4.0. Use `availableForPurchaseAt` field instead.
*/ */
availableForPurchaseDate?: InputMaybe<Scalars['Date']>; availableForPurchaseDate?: InputMaybe<Scalars['Date']>;
/** Added in Saleor 3.3. A start date time from which a product will be available for purchase. When not set and `isAvailable` is set to True, the current day is assumed. */ /**
* A start date time from which a product will be available for purchase. When not set and `isAvailable` is set to True, the current day is assumed.
*
* Added in Saleor 3.3.
*/
availableForPurchaseAt?: InputMaybe<Scalars['DateTime']>; availableForPurchaseAt?: InputMaybe<Scalars['DateTime']>;
/** List of variants to which the channel should be assigned. */ /** List of variants to which the channel should be assigned. */
addVariants?: InputMaybe<Array<Scalars['ID']>>; addVariants?: InputMaybe<Array<Scalars['ID']>>;
@ -3631,9 +3741,21 @@ export type ProductVariantBulkCreateInput = {
trackInventory?: InputMaybe<Scalars['Boolean']>; trackInventory?: InputMaybe<Scalars['Boolean']>;
/** Weight of the Product Variant. */ /** Weight of the Product Variant. */
weight?: InputMaybe<Scalars['WeightScalar']>; weight?: InputMaybe<Scalars['WeightScalar']>;
/** Added in Saleor 3.1. Determines if variant is in preorder. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Determines if variant is in preorder.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
preorder?: InputMaybe<PreorderSettingsInput>; preorder?: InputMaybe<PreorderSettingsInput>;
/** Added in Saleor 3.1. Determines maximum quantity of `ProductVariant`,that can be bought in a single checkout. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Determines maximum quantity of `ProductVariant`,that can be bought in a single checkout.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
quantityLimitPerCustomer?: InputMaybe<Scalars['Int']>; quantityLimitPerCustomer?: InputMaybe<Scalars['Int']>;
/** Stocks of a product available for sale. */ /** Stocks of a product available for sale. */
stocks?: InputMaybe<Array<StockInput>>; stocks?: InputMaybe<Array<StockInput>>;
@ -3648,7 +3770,13 @@ export type ProductVariantChannelListingAddInput = {
price: Scalars['PositiveDecimal']; price: Scalars['PositiveDecimal'];
/** Cost price of the variant in channel. */ /** Cost price of the variant in channel. */
costPrice?: InputMaybe<Scalars['PositiveDecimal']>; costPrice?: InputMaybe<Scalars['PositiveDecimal']>;
/** Added in Saleor 3.1. The threshold for preorder variant in channel. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* The threshold for preorder variant in channel.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
preorderThreshold?: InputMaybe<Scalars['Int']>; preorderThreshold?: InputMaybe<Scalars['Int']>;
}; };
@ -3661,9 +3789,21 @@ export type ProductVariantCreateInput = {
trackInventory?: InputMaybe<Scalars['Boolean']>; trackInventory?: InputMaybe<Scalars['Boolean']>;
/** Weight of the Product Variant. */ /** Weight of the Product Variant. */
weight?: InputMaybe<Scalars['WeightScalar']>; weight?: InputMaybe<Scalars['WeightScalar']>;
/** Added in Saleor 3.1. Determines if variant is in preorder. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Determines if variant is in preorder.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
preorder?: InputMaybe<PreorderSettingsInput>; preorder?: InputMaybe<PreorderSettingsInput>;
/** Added in Saleor 3.1. Determines maximum quantity of `ProductVariant`,that can be bought in a single checkout. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Determines maximum quantity of `ProductVariant`,that can be bought in a single checkout.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
quantityLimitPerCustomer?: InputMaybe<Scalars['Int']>; quantityLimitPerCustomer?: InputMaybe<Scalars['Int']>;
/** Product ID of which type is the variant. */ /** Product ID of which type is the variant. */
product: Scalars['ID']; product: Scalars['ID'];
@ -3688,9 +3828,21 @@ export type ProductVariantInput = {
trackInventory?: InputMaybe<Scalars['Boolean']>; trackInventory?: InputMaybe<Scalars['Boolean']>;
/** Weight of the Product Variant. */ /** Weight of the Product Variant. */
weight?: InputMaybe<Scalars['WeightScalar']>; weight?: InputMaybe<Scalars['WeightScalar']>;
/** Added in Saleor 3.1. Determines if variant is in preorder. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Determines if variant is in preorder.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
preorder?: InputMaybe<PreorderSettingsInput>; preorder?: InputMaybe<PreorderSettingsInput>;
/** Added in Saleor 3.1. Determines maximum quantity of `ProductVariant`,that can be bought in a single checkout. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Determines maximum quantity of `ProductVariant`,that can be bought in a single checkout.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
quantityLimitPerCustomer?: InputMaybe<Scalars['Int']>; quantityLimitPerCustomer?: InputMaybe<Scalars['Int']>;
}; };
@ -3717,7 +3869,11 @@ export type PublishableChannelListingInput = {
* DEPRECATED: this field will be removed in Saleor 4.0. Use `publishedAt` field instead. * DEPRECATED: this field will be removed in Saleor 4.0. Use `publishedAt` field instead.
*/ */
publicationDate?: InputMaybe<Scalars['Date']>; publicationDate?: InputMaybe<Scalars['Date']>;
/** Added in Saleor 3.3. Publication date time. ISO 8601 standard. */ /**
* Publication date time. ISO 8601 standard.
*
* Added in Saleor 3.3.
*/
publishedAt?: InputMaybe<Scalars['DateTime']>; publishedAt?: InputMaybe<Scalars['DateTime']>;
}; };
@ -3964,9 +4120,17 @@ export type ShopSettingsInput = {
defaultWeightUnit?: InputMaybe<WeightUnitsEnum>; defaultWeightUnit?: InputMaybe<WeightUnitsEnum>;
/** Enable automatic fulfillment for all digital products. */ /** Enable automatic fulfillment for all digital products. */
automaticFulfillmentDigitalProducts?: InputMaybe<Scalars['Boolean']>; automaticFulfillmentDigitalProducts?: InputMaybe<Scalars['Boolean']>;
/** Added in Saleor 3.1. Enable automatic approval of all new fulfillments. */ /**
* Enable automatic approval of all new fulfillments.
*
* Added in Saleor 3.1.
*/
fulfillmentAutoApprove?: InputMaybe<Scalars['Boolean']>; fulfillmentAutoApprove?: InputMaybe<Scalars['Boolean']>;
/** Added in Saleor 3.1. Enable ability to approve fulfillments which are unpaid. */ /**
* Enable ability to approve fulfillments which are unpaid.
*
* Added in Saleor 3.1.
*/
fulfillmentAllowUnpaid?: InputMaybe<Scalars['Boolean']>; fulfillmentAllowUnpaid?: InputMaybe<Scalars['Boolean']>;
/** Default number of max downloads per digital content URL. */ /** Default number of max downloads per digital content URL. */
defaultDigitalMaxDownloads?: InputMaybe<Scalars['Int']>; defaultDigitalMaxDownloads?: InputMaybe<Scalars['Int']>;
@ -3978,11 +4142,25 @@ export type ShopSettingsInput = {
defaultMailSenderAddress?: InputMaybe<Scalars['String']>; defaultMailSenderAddress?: InputMaybe<Scalars['String']>;
/** URL of a view where customers can set their password. */ /** URL of a view where customers can set their password. */
customerSetPasswordUrl?: InputMaybe<Scalars['String']>; customerSetPasswordUrl?: InputMaybe<Scalars['String']>;
/** Added in Saleor 3.1. Default number of minutes stock will be reserved for anonymous checkout. Enter 0 or null to disable. */ /**
* Default number of minutes stock will be reserved for anonymous checkout. Enter 0 or null to disable.
*
* Added in Saleor 3.1.
*/
reserveStockDurationAnonymousUser?: InputMaybe<Scalars['Int']>; reserveStockDurationAnonymousUser?: InputMaybe<Scalars['Int']>;
/** Added in Saleor 3.1. Default number of minutes stock will be reserved for authenticated checkout. Enter 0 or null to disable. */ /**
* Default number of minutes stock will be reserved for authenticated checkout. Enter 0 or null to disable.
*
* Added in Saleor 3.1.
*/
reserveStockDurationAuthenticatedUser?: InputMaybe<Scalars['Int']>; reserveStockDurationAuthenticatedUser?: InputMaybe<Scalars['Int']>;
/** Added in Saleor 3.1. Default number of maximum line quantity in single checkout. Minimum possible value is 1, default value is 50. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Default number of maximum line quantity in single checkout. Minimum possible value is 1, default value is 50.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
limitQuantityPerCheckout?: InputMaybe<Scalars['Int']>; limitQuantityPerCheckout?: InputMaybe<Scalars['Int']>;
}; };
@ -4275,7 +4453,11 @@ export type VoucherInput = {
discountValueType?: InputMaybe<DiscountValueTypeEnum>; discountValueType?: InputMaybe<DiscountValueTypeEnum>;
/** Products discounted by the voucher. */ /** Products discounted by the voucher. */
products?: InputMaybe<Array<Scalars['ID']>>; products?: InputMaybe<Array<Scalars['ID']>>;
/** Added in Saleor 3.1. Variants discounted by the voucher. */ /**
* Variants discounted by the voucher.
*
* Added in Saleor 3.1.
*/
variants?: InputMaybe<Array<Scalars['ID']>>; variants?: InputMaybe<Array<Scalars['ID']>>;
/** Collections discounted by the voucher. */ /** Collections discounted by the voucher. */
collections?: InputMaybe<Array<Scalars['ID']>>; collections?: InputMaybe<Array<Scalars['ID']>>;
@ -4389,9 +4571,21 @@ export type WarehouseUpdateInput = {
name?: InputMaybe<Scalars['String']>; name?: InputMaybe<Scalars['String']>;
/** Address of the warehouse. */ /** Address of the warehouse. */
address?: InputMaybe<AddressInput>; address?: InputMaybe<AddressInput>;
/** Added in Saleor 3.1. Click and collect options: local, all or disabled. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Click and collect options: local, all or disabled.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
clickAndCollectOption?: InputMaybe<WarehouseClickAndCollectOptionEnum>; clickAndCollectOption?: InputMaybe<WarehouseClickAndCollectOptionEnum>;
/** Added in Saleor 3.1. Visibility of warehouse stocks. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Visibility of warehouse stocks.
*
* Added in Saleor 3.1.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
isPrivate?: InputMaybe<Scalars['Boolean']>; isPrivate?: InputMaybe<Scalars['Boolean']>;
}; };
@ -4416,7 +4610,13 @@ export type WebhookCreateInput = {
isActive?: InputMaybe<Scalars['Boolean']>; isActive?: InputMaybe<Scalars['Boolean']>;
/** The secret key used to create a hash signature with each payload. */ /** The secret key used to create a hash signature with each payload. */
secretKey?: InputMaybe<Scalars['String']>; secretKey?: InputMaybe<Scalars['String']>;
/** Added in Saleor 3.2. Subscription query used to define a webhook payload. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Subscription query used to define a webhook payload.
*
* Added in Saleor 3.2.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
query?: InputMaybe<Scalars['String']>; query?: InputMaybe<Scalars['String']>;
}; };
@ -4745,7 +4945,13 @@ export type WebhookUpdateInput = {
isActive?: InputMaybe<Scalars['Boolean']>; isActive?: InputMaybe<Scalars['Boolean']>;
/** Use to create a hash signature with each payload. */ /** Use to create a hash signature with each payload. */
secretKey?: InputMaybe<Scalars['String']>; secretKey?: InputMaybe<Scalars['String']>;
/** Added in Saleor 3.2. Subscription query used to define a webhook payload. Note: this feature is in a preview state and can be subject to changes at later point. */ /**
* Subscription query used to define a webhook payload.
*
* Added in Saleor 3.2.
*
* Note: this API is currently in Feature Preview and can be subject to changes at later point.
*/
query?: InputMaybe<Scalars['String']>; query?: InputMaybe<Scalars['String']>;
}; };
@ -5826,6 +6032,8 @@ export type ProductVariantFragment = { __typename: 'ProductVariant', id: string,
export type ExportFileFragment = { __typename: 'ExportFile', id: string, status: JobStatusEnum, url: string | null }; export type ExportFileFragment = { __typename: 'ExportFile', id: string, status: JobStatusEnum, url: string | null };
export type ProductListAttributeFragment = { __typename: 'SelectedAttribute', attribute: { __typename: 'Attribute', id: string }, values: Array<{ __typename: 'AttributeValue', id: string, name: string | null, slug: string | null, reference: string | null, boolean: boolean | null, date: any | null, dateTime: any | null, value: string | null, file: { __typename: 'File', url: string, contentType: string | null } | null }> };
export type ShippingZoneFragment = { __typename: 'ShippingZone', id: string, name: string, description: string | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }>, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> }; export type ShippingZoneFragment = { __typename: 'ShippingZone', id: string, name: string, description: string | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }>, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> };
export type ShippingMethodWithPostalCodesFragment = { __typename: 'ShippingMethodType', id: string, postalCodeRules: Array<{ __typename: 'ShippingMethodPostalCodeRule', id: string, inclusionType: PostalCodeRuleInclusionTypeEnum | null, start: string | null, end: string | null }> | null }; export type ShippingMethodWithPostalCodesFragment = { __typename: 'ShippingMethodType', id: string, postalCodeRules: Array<{ __typename: 'ShippingMethodPostalCodeRule', id: string, inclusionType: PostalCodeRuleInclusionTypeEnum | null, start: string | null, end: string | null }> | null };
@ -7020,14 +7228,6 @@ export type ProductMediaByIdQueryVariables = Exact<{
export type ProductMediaByIdQuery = { __typename: 'Query', product: { __typename: 'Product', id: string, name: string, mainImage: { __typename: 'ProductMedia', id: string, alt: string, url: string, type: ProductMediaType, oembedData: any } | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, alt: string, type: ProductMediaType, oembedData: any }> | null } | null }; export type ProductMediaByIdQuery = { __typename: 'Query', product: { __typename: 'Product', id: string, name: string, mainImage: { __typename: 'ProductMedia', id: string, alt: string, url: string, type: ProductMediaType, oembedData: any } | null, media: Array<{ __typename: 'ProductMedia', id: string, url: string, alt: string, type: ProductMediaType, oembedData: any }> | null } | null };
export type AvailableInGridAttributesQueryVariables = Exact<{
first: Scalars['Int'];
after?: InputMaybe<Scalars['String']>;
}>;
export type AvailableInGridAttributesQuery = { __typename: 'Query', availableInGrid: { __typename: 'AttributeCountableConnection', totalCount: number | null, edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null };
export type GridAttributesQueryVariables = Exact<{ export type GridAttributesQueryVariables = Exact<{
ids: Array<Scalars['ID']> | Scalars['ID']; ids: Array<Scalars['ID']> | Scalars['ID'];
}>; }>;
@ -7065,6 +7265,15 @@ export type SearchAttributeValuesQueryVariables = Exact<{
export type SearchAttributeValuesQuery = { __typename: 'Query', attribute: { __typename: 'Attribute', id: string, choices: { __typename: 'AttributeValueCountableConnection', edges: Array<{ __typename: 'AttributeValueCountableEdge', node: { __typename: 'AttributeValue', richText: any | null, id: string, name: string | null, slug: string | null, reference: string | null, boolean: boolean | null, date: any | null, dateTime: any | null, value: string | null, file: { __typename: 'File', url: string, contentType: string | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null } | null }; export type SearchAttributeValuesQuery = { __typename: 'Query', attribute: { __typename: 'Attribute', id: string, choices: { __typename: 'AttributeValueCountableConnection', edges: Array<{ __typename: 'AttributeValueCountableEdge', node: { __typename: 'AttributeValue', richText: any | null, id: string, name: string | null, slug: string | null, reference: string | null, boolean: boolean | null, date: any | null, dateTime: any | null, value: string | null, file: { __typename: 'File', url: string, contentType: string | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null } | null };
export type SearchAvailableInGridAttributesQueryVariables = Exact<{
first: Scalars['Int'];
after?: InputMaybe<Scalars['String']>;
query: Scalars['String'];
}>;
export type SearchAvailableInGridAttributesQuery = { __typename: 'Query', availableInGrid: { __typename: 'AttributeCountableConnection', totalCount: number | null, edges: Array<{ __typename: 'AttributeCountableEdge', node: { __typename: 'Attribute', id: string, name: string | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null };
export type SearchAvailablePageAttributesQueryVariables = Exact<{ export type SearchAvailablePageAttributesQueryVariables = Exact<{
id: Scalars['ID']; id: Scalars['ID'];
after?: InputMaybe<Scalars['String']>; after?: InputMaybe<Scalars['String']>;

View file

@ -15,6 +15,7 @@ export interface UseSearchResult<TData, TVariables extends SearchVariables> {
loadMore: () => void; loadMore: () => void;
result: QueryResult<TData, TVariables>; result: QueryResult<TData, TVariables>;
search: (query: string) => void; search: (query: string) => void;
query: string;
} }
export type UseSearchOpts<TVariables extends SearchVariables> = Partial<{ export type UseSearchOpts<TVariables extends SearchVariables> = Partial<{
skip: boolean; skip: boolean;
@ -45,6 +46,7 @@ function makeSearch<TData, TVariables extends SearchVariables>(
}); });
return { return {
query: searchQuery,
loadMore: () => loadMoreFn(result), loadMore: () => loadMoreFn(result),
result, result,
search: debouncedSearch search: debouncedSearch

View file

@ -20,7 +20,10 @@ export interface ChangeEvent<TData = any> {
} }
export type SubmitPromise<TData = any> = Promise<TData>; export type SubmitPromise<TData = any> = Promise<TData>;
export type FormChange = (event: ChangeEvent, cb?: () => void) => void; export type FormChange<T = any> = (
event: ChangeEvent<T>,
cb?: () => void
) => void;
export type FormErrors<T> = { export type FormErrors<T> = {
[field in keyof T]?: string | React.ReactNode; [field in keyof T]?: string | React.ReactNode;

View file

@ -221,6 +221,10 @@ export const buttonMessages = defineMessages({
defaultMessage: "Remove", defaultMessage: "Remove",
description: "button" description: "button"
}, },
reset: {
defaultMessage: "Reset",
description: "button"
},
save: { save: {
defaultMessage: "Save", defaultMessage: "Save",
description: "button" description: "button"

View file

@ -48,6 +48,7 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { columnsMessages } from "./messages"; import { columnsMessages } from "./messages";
import ProductListAttribute from "./ProductListAttribute";
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => ({
@ -69,7 +70,10 @@ const useStyles = makeStyles(
} }
}, },
colAttribute: { colAttribute: {
width: 150 width: 200,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}, },
colFill: { colFill: {
padding: 0, padding: 0,
@ -123,7 +127,6 @@ interface ProductListProps
activeAttributeSortId: string; activeAttributeSortId: string;
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>; gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
products: RelayToFlat<ProductListQuery["products"]>; products: RelayToFlat<ProductListQuery["products"]>;
loading: boolean;
} }
export const ProductList: React.FC<ProductListProps> = props => { export const ProductList: React.FC<ProductListProps> = props => {
@ -427,19 +430,10 @@ export const ProductList: React.FC<ProductListProps> = props => {
gridAttribute gridAttribute
)} )}
> >
{maybe<React.ReactNode>(() => { <ProductListAttribute
const attribute = product.attributes.find( attribute={gridAttribute}
attribute => productAttributes={product?.attributes}
attribute.attribute.id === />
getAttributeIdFromColumnValue(gridAttribute)
);
if (attribute) {
return attribute.values
.map(value => value.name)
.join(", ");
}
return "-";
}, <Skeleton />)}
</TableCell> </TableCell>
))} ))}
<DisplayColumn <DisplayColumn

View file

@ -0,0 +1,105 @@
import { ProductListAttributeFragment } from "@saleor/graphql";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import ProductListAttribute from "./ProductListAttribute";
const attributes: ProductListAttributeFragment[] = [
{
__typename: "SelectedAttribute",
attribute: {
__typename: "Attribute",
id: "1"
},
values: [
{
id: "QXR0cmlidXRlVmFsdWU6MTEz",
name: "2022-03-11",
slug: "72_37",
file: null,
reference: null,
boolean: null,
date: "2022-03-11",
dateTime: null,
value: "",
__typename: "AttributeValue"
}
]
},
{
attribute: {
id: "2",
__typename: "Attribute"
},
values: [
{
id: "QXR0cmlidXRlVmFsdWU6MTE1",
name: "2022-03-01 16:24:00+01:00",
slug: "74_38",
file: null,
reference: null,
boolean: null,
date: null,
dateTime: "2022-03-01T15:24:00+00:00",
value: "",
__typename: "AttributeValue"
}
],
__typename: "SelectedAttribute"
},
{
attribute: {
id: "3",
__typename: "Attribute"
},
values: [
{
id: "QXR0cmlidXRlOjMw",
name: "Lorem Ipsum",
slug: "72_2",
file: null,
reference: "UGFnZToy",
boolean: null,
date: null,
dateTime: null,
value: "",
__typename: "AttributeValue"
},
{
id: "QXR0cmlidXRlOjMx",
name: "Dolor Sit",
slug: "72_3",
file: null,
reference: "UGFnZToz",
boolean: null,
date: null,
dateTime: null,
value: "",
__typename: "AttributeValue"
}
],
__typename: "SelectedAttribute"
}
];
storiesOf("Views / Products / Product list / Attribute display", module)
.addDecorator(Decorator)
.add("default", () => (
<ProductListAttribute
attribute="attribute:3"
productAttributes={attributes}
/>
))
.add("date", () => (
<ProductListAttribute
attribute="attribute:1"
productAttributes={attributes}
/>
))
.add("datetime", () => (
<ProductListAttribute
attribute="attribute:2"
productAttributes={attributes}
/>
));

View file

@ -0,0 +1,44 @@
import Date, { DateTime } from "@saleor/components/Date";
import Skeleton from "@saleor/components/Skeleton";
import { ProductListAttributeFragment } from "@saleor/graphql";
import React from "react";
import { getAttributeIdFromColumnValue } from "../ProductListPage/utils";
export interface ProductListAttributeProps {
attribute: string;
productAttributes: ProductListAttributeFragment[];
}
const ProductListAttribute: React.FC<ProductListAttributeProps> = ({
attribute: gridAttribute,
productAttributes
}) => {
if (!productAttributes) {
return <Skeleton />;
}
const productAttribute = productAttributes.find(
attribute =>
attribute.attribute.id === getAttributeIdFromColumnValue(gridAttribute)
);
if (productAttribute) {
if (productAttribute.values.length) {
if (productAttribute.values[0].date) {
return <Date date={productAttribute.values[0].date} />;
}
if (productAttribute.values[0].dateTime) {
return <DateTime date={productAttribute.values[0].dateTime} />;
}
}
const textValue = productAttribute.values
.map(value => value.name)
.join(", ");
return <span title={textValue}>{textValue}</span>;
}
return <span>-</span>;
};
export default ProductListAttribute;

View file

@ -6,20 +6,19 @@ import {
} from "@saleor/apps/useExtensions"; } from "@saleor/apps/useExtensions";
import { ButtonWithSelect } from "@saleor/components/ButtonWithSelect"; import { ButtonWithSelect } from "@saleor/components/ButtonWithSelect";
import CardMenu from "@saleor/components/CardMenu"; import CardMenu from "@saleor/components/CardMenu";
import ColumnPicker, { import ColumnPicker from "@saleor/components/ColumnPicker";
ColumnPickerChoice
} from "@saleor/components/ColumnPicker";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import { getByName } from "@saleor/components/Filter/utils"; import { getByName } from "@saleor/components/Filter/utils";
import FilterBar from "@saleor/components/FilterBar"; import FilterBar from "@saleor/components/FilterBar";
import LimitReachedAlert from "@saleor/components/LimitReachedAlert"; import LimitReachedAlert from "@saleor/components/LimitReachedAlert";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import { ProductListColumns } from "@saleor/config"; import { ProductListColumns } from "@saleor/config";
import { import {
AvailableInGridAttributesQuery,
GridAttributesQuery, GridAttributesQuery,
ProductListQuery, ProductListQuery,
RefreshLimitsQuery RefreshLimitsQuery,
SearchAvailableInGridAttributesQuery
} from "@saleor/graphql"; } from "@saleor/graphql";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { makeStyles } from "@saleor/macaw-ui"; import { makeStyles } from "@saleor/macaw-ui";
@ -44,6 +43,7 @@ import {
ProductFilterKeys, ProductFilterKeys,
ProductListFilterOpts ProductListFilterOpts
} from "./filters"; } from "./filters";
import { getAttributeColumnValue } from "./utils";
export interface ProductListPageProps export interface ProductListPageProps
extends PageListProps<ProductListColumns>, extends PageListProps<ProductListColumns>,
@ -54,15 +54,17 @@ export interface ProductListPageProps
ChannelProps { ChannelProps {
activeAttributeSortId: string; activeAttributeSortId: string;
availableInGridAttributes: RelayToFlat< availableInGridAttributes: RelayToFlat<
AvailableInGridAttributesQuery["availableInGrid"] SearchAvailableInGridAttributesQuery["availableInGrid"]
>; >;
channelsCount: number; channelsCount: number;
columnQuery: string;
currencySymbol: string; currencySymbol: string;
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>; gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
limits: RefreshLimitsQuery["shop"]["limits"]; limits: RefreshLimitsQuery["shop"]["limits"];
totalGridAttributes: number; totalGridAttributes: number;
products: RelayToFlat<ProductListQuery["products"]>; products: RelayToFlat<ProductListQuery["products"]>;
onExport: () => void; onExport: () => void;
onColumnQueryChange: (query: string) => void;
} }
const useStyles = makeStyles( const useStyles = makeStyles(
@ -87,6 +89,7 @@ const useStyles = makeStyles(
export const ProductListPage: React.FC<ProductListPageProps> = props => { export const ProductListPage: React.FC<ProductListPageProps> = props => {
const { const {
channelsCount, channelsCount,
columnQuery,
currencySymbol, currencySymbol,
currentTab, currentTab,
defaultSettings, defaultSettings,
@ -102,6 +105,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
totalGridAttributes, totalGridAttributes,
onAdd, onAdd,
onAll, onAll,
onColumnQueryChange,
onExport, onExport,
onFetchMore, onFetchMore,
onFilterChange, onFilterChange,
@ -117,14 +121,11 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
const intl = useIntl(); const intl = useIntl();
const classes = useStyles(props); const classes = useStyles(props);
const handleSave = (columns: ProductListColumns[]) => const staticColumns = [
onUpdateListSettings("columns", columns); {
label: intl.formatMessage(columnsMessages.availability),
const filterStructure = createFilterStructure(intl, filterOpts); value: "availability" as ProductListColumns
},
const filterDependency = filterStructure.find(getByName("channel"));
const columns: ColumnPickerChoice[] = [
{ {
label: intl.formatMessage(columnsMessages.price), label: intl.formatMessage(columnsMessages.price),
value: "price" as ProductListColumns value: "price" as ProductListColumns
@ -136,11 +137,37 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
{ {
label: intl.formatMessage(columnsMessages.updatedAt), label: intl.formatMessage(columnsMessages.updatedAt),
value: "date" as ProductListColumns value: "date" as ProductListColumns
}, }
...availableInGridAttributes.map(attribute => ({ ];
const initialColumnsChoices = React.useMemo(() => {
const selectedStaticColumns = staticColumns.filter(column =>
(settings.columns || []).includes(column.value)
);
const selectedAttributeColumns = gridAttributes.map(attribute => ({
label: attribute.name, label: attribute.name,
value: `attribute:${attribute.id}` value: getAttributeColumnValue(attribute.id)
})) }));
return [...selectedStaticColumns, ...selectedAttributeColumns];
}, [gridAttributes, settings.columns]);
const handleSave = (columns: ProductListColumns[]) =>
onUpdateListSettings("columns", columns);
const filterStructure = createFilterStructure(intl, filterOpts);
const filterDependency = filterStructure.find(getByName("channel"));
const availableColumns: MultiAutocompleteChoiceType[] = [
...staticColumns,
...availableInGridAttributes.map(
attribute =>
({
label: attribute.name,
value: getAttributeColumnValue(attribute.id)
} as MultiAutocompleteChoiceType)
)
]; ];
const limitReached = isLimitReached(limits, "productVariants"); const limitReached = isLimitReached(limits, "productVariants");
@ -189,15 +216,13 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
> >
<ColumnPicker <ColumnPicker
className={classes.columnPicker} className={classes.columnPicker}
columns={columns} availableColumns={availableColumns}
initialColumns={initialColumnsChoices}
defaultColumns={defaultSettings.columns} defaultColumns={defaultSettings.columns}
hasMore={hasMore} hasMore={hasMore}
initialColumns={settings.columns} loading={loading}
total={ query={columnQuery}
columns.length - onQueryChange={onColumnQueryChange}
availableInGridAttributes.length +
totalGridAttributes
}
onFetchMore={onFetchMore} onFetchMore={onFetchMore}
onSave={handleSave} onSave={handleSave}
/> />
@ -247,7 +272,6 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
/> />
<ProductList <ProductList
{...listProps} {...listProps}
loading={loading}
gridAttributes={gridAttributes} gridAttributes={gridAttributes}
settings={settings} settings={settings}
selectedChannelId={selectedChannelId} selectedChannelId={selectedChannelId}

View file

@ -2,10 +2,7 @@ import { gql } from "@apollo/client";
export const initialProductFilterAttributesQuery = gql` export const initialProductFilterAttributesQuery = gql`
query InitialProductFilterAttributes { query InitialProductFilterAttributes {
attributes( attributes(first: 100, filter: { type: PRODUCT_TYPE }) {
first: 100
filter: { filterableInDashboard: true, type: PRODUCT_TYPE }
) {
edges { edges {
node { node {
id id
@ -83,12 +80,7 @@ export const productListQuery = gql`
...ProductWithChannelListings ...ProductWithChannelListings
updatedAt updatedAt
attributes @include(if: $hasSelectedAttributes) { attributes @include(if: $hasSelectedAttributes) {
attribute { ...ProductListAttribute
id
}
values {
...AttributeValue
}
} }
} }
} }
@ -253,31 +245,6 @@ export const productMediaQuery = gql`
} }
`; `;
export const availableInGridAttributes = gql`
query AvailableInGridAttributes($first: Int!, $after: String) {
availableInGrid: attributes(
first: $first
after: $after
filter: {
availableInGrid: true
isVariantOnly: false
type: PRODUCT_TYPE
}
) {
edges {
node {
id
name
}
}
pageInfo {
...PageInfo
}
totalCount
}
}
`;
export const gridAttributes = gql` export const gridAttributes = gql`
query GridAttributes($ids: [ID!]!) { query GridAttributes($ids: [ID!]!) {
grid: attributes(first: 25, filter: { ids: $ids }) { grid: attributes(first: 25, filter: { ids: $ids }) {

View file

@ -1,4 +1,5 @@
import { DialogContentText } from "@material-ui/core"; import { DialogContentText } from "@material-ui/core";
import { filterable } from "@saleor/attributes/utils/data";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import useAppChannel from "@saleor/components/AppLayout/AppChannelContext"; import useAppChannel from "@saleor/components/AppLayout/AppChannelContext";
import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
@ -15,7 +16,6 @@ import {
import { Task } from "@saleor/containers/BackgroundTasks/types"; import { Task } from "@saleor/containers/BackgroundTasks/types";
import { import {
ProductListQueryVariables, ProductListQueryVariables,
useAvailableInGridAttributesQuery,
useGridAttributesQuery, useGridAttributesQuery,
useInitialProductFilterAttributesQuery, useInitialProductFilterAttributesQuery,
useInitialProductFilterCategoriesQuery, useInitialProductFilterCategoriesQuery,
@ -54,6 +54,7 @@ import {
} from "@saleor/products/urls"; } from "@saleor/products/urls";
import useAttributeSearch from "@saleor/searches/useAttributeSearch"; import useAttributeSearch from "@saleor/searches/useAttributeSearch";
import useAttributeValueSearch from "@saleor/searches/useAttributeValueSearch"; import useAttributeValueSearch from "@saleor/searches/useAttributeValueSearch";
import useAvailableInGridAttributesSearch from "@saleor/searches/useAvailableInGridAttributesSearch";
import useCategorySearch from "@saleor/searches/useCategorySearch"; import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch"; import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import useProductTypeSearch from "@saleor/searches/useProductTypeSearch"; import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
@ -276,12 +277,9 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
[params, settings.rowNumber] [params, settings.rowNumber]
); );
function filterColumnIds(columns: ProductListColumns[]) { const filteredColumnIds = settings.columns
return columns .filter(isAttributeColumnValue)
.filter(isAttributeColumnValue) .map(getAttributeIdFromColumnValue);
.map(getAttributeIdFromColumnValue);
}
const filteredColumnIds = filterColumnIds(settings.columns);
const { data, loading, refetch } = useProductListQuery({ const { data, loading, refetch } = useProductListQuery({
displayLoader: true, displayLoader: true,
@ -292,11 +290,15 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
} }
}); });
const availableInGridAttributes = useAvailableInGridAttributesQuery({ const availableInGridAttributesOpts = useAvailableInGridAttributesSearch({
variables: { first: 24 } variables: {
...DEFAULT_INITIAL_SEARCH_DATA,
first: 5
}
}); });
const gridAttributes = useGridAttributesQuery({ const gridAttributes = useGridAttributesQuery({
variables: { ids: filteredColumnIds } variables: { ids: filteredColumnIds },
skip: filteredColumnIds.length === 0
}); });
const [ const [
@ -319,7 +321,9 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
const filterOpts = getFilterOpts( const filterOpts = getFilterOpts(
params, params,
mapEdgesToItems(initialFilterAttributes?.attributes) || [], (mapEdgesToItems(initialFilterAttributes?.attributes) || []).filter(
filterable
),
searchAttributeValues, searchAttributeValues,
{ {
initial: mapEdgesToItems(initialFilterCategories?.categories) || [], initial: mapEdgesToItems(initialFilterCategories?.categories) || [],
@ -353,8 +357,9 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
}} }}
onSort={handleSort} onSort={handleSort}
availableInGridAttributes={ availableInGridAttributes={
mapEdgesToItems(availableInGridAttributes?.data?.availableInGrid) || mapEdgesToItems(
[] availableInGridAttributesOpts.result?.data?.availableInGrid
) || []
} }
currencySymbol={selectedChannel?.currencyCode || ""} currencySymbol={selectedChannel?.currencyCode || ""}
currentTab={currentTab} currentTab={currentTab}
@ -362,48 +367,27 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
filterOpts={filterOpts} filterOpts={filterOpts}
gridAttributes={mapEdgesToItems(gridAttributes?.data?.grid) || []} gridAttributes={mapEdgesToItems(gridAttributes?.data?.grid) || []}
totalGridAttributes={maybe( totalGridAttributes={maybe(
() => availableInGridAttributes.data.availableInGrid.totalCount, () =>
availableInGridAttributesOpts.result.data.availableInGrid
.totalCount,
0 0
)} )}
settings={settings} settings={settings}
loading={availableInGridAttributes.loading || gridAttributes.loading} loading={
availableInGridAttributesOpts.result.loading || gridAttributes.loading
}
hasMore={maybe( hasMore={maybe(
() => () =>
availableInGridAttributes.data.availableInGrid.pageInfo.hasNextPage, availableInGridAttributesOpts.result.data.availableInGrid.pageInfo
.hasNextPage,
false false
)} )}
onAdd={() => navigate(productAddUrl())} onAdd={() => navigate(productAddUrl())}
disabled={loading} disabled={loading}
limits={limitOpts.data?.shop.limits} limits={limitOpts.data?.shop.limits}
products={mapEdgesToItems(data?.products)} products={mapEdgesToItems(data?.products)}
onFetchMore={() => onColumnQueryChange={availableInGridAttributesOpts.search}
availableInGridAttributes.loadMore( onFetchMore={availableInGridAttributesOpts.loadMore}
(prev, next) => {
if (
prev.availableInGrid.pageInfo.endCursor ===
next.availableInGrid.pageInfo.endCursor
) {
return prev;
}
return {
...prev,
availableInGrid: {
...prev.availableInGrid,
edges: [
...prev.availableInGrid.edges,
...next.availableInGrid.edges
],
pageInfo: next.availableInGrid.pageInfo
}
};
},
{
after:
availableInGridAttributes.data.availableInGrid.pageInfo
.endCursor
}
)
}
onNextPage={loadNextPage} onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings} onUpdateListSettings={updateListSettings}
@ -438,6 +422,7 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
onExport={() => openModal("export")} onExport={() => openModal("export")}
channelsCount={availableChannels?.length} channelsCount={availableChannels?.length}
selectedChannelId={selectedChannel?.id} selectedChannelId={selectedChannel?.id}
columnQuery={availableInGridAttributesOpts.query}
/> />
<ActionDialog <ActionDialog
open={params.action === "delete"} open={params.action === "delete"}

View file

@ -0,0 +1,66 @@
import { gql } from "@apollo/client";
import {
SearchAvailableInGridAttributesDocument,
SearchAvailableInGridAttributesQuery,
SearchAvailableInGridAttributesQueryVariables
} from "@saleor/graphql";
import makeSearch from "@saleor/hooks/makeSearch";
export const availableInGridAttributes = gql`
query SearchAvailableInGridAttributes(
$first: Int!
$after: String
$query: String!
) {
availableInGrid: attributes(
first: $first
after: $after
filter: { isVariantOnly: false, type: PRODUCT_TYPE, search: $query }
) {
edges {
node {
id
name
}
}
pageInfo {
...PageInfo
}
totalCount
}
}
`;
export default makeSearch<
SearchAvailableInGridAttributesQuery,
SearchAvailableInGridAttributesQueryVariables
>(SearchAvailableInGridAttributesDocument, result => {
if (result.data?.availableInGrid.pageInfo.hasNextPage) {
result.loadMore(
(prev, next) => {
if (
prev.availableInGrid.pageInfo.endCursor ===
next.availableInGrid.pageInfo.endCursor
) {
return prev;
}
return {
...prev,
availableInGrid: {
...prev.availableInGrid,
edges: [
...prev.availableInGrid.edges,
...next.availableInGrid.edges
],
pageInfo: next.availableInGrid.pageInfo
}
} as SearchAvailableInGridAttributesQuery;
},
{
...result.variables,
after: result.data.availableInGrid.pageInfo.endCursor
}
);
}
});

File diff suppressed because it is too large Load diff

View file

@ -24,14 +24,6 @@ const props: AttributeListPageProps = {
...filterPageProps, ...filterPageProps,
attributes, attributes,
filterOpts: { filterOpts: {
availableInGrid: {
active: false,
value: false
},
filterableInDashboard: {
active: false,
value: false
},
filterableInStorefront: { filterableInStorefront: {
active: false, active: false,
value: false value: false

View file

@ -1,14 +1,14 @@
import ColumnPicker, { import ColumnPicker, {
ColumnPickerProps ColumnPickerProps
} from "@saleor/components/ColumnPicker"; } from "@saleor/components/ColumnPicker";
import { ColumnPickerChoice } from "@saleor/components/ColumnPicker/ColumnPickerContent"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import CardDecorator from "@saleor/storybook/CardDecorator"; import CardDecorator from "@saleor/storybook/CardDecorator";
import { storiesOf } from "@storybook/react"; import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import Decorator from "../../Decorator"; import Decorator from "../../Decorator";
const columns: ColumnPickerChoice[] = [ const availableColumns: MultiAutocompleteChoiceType[] = [
{ label: "Name", value: "name" }, { label: "Name", value: "name" },
{ label: "Value", value: "value" }, { label: "Value", value: "value" },
{ label: "Type", value: "type" }, { label: "Type", value: "type" },
@ -25,11 +25,16 @@ const columns: ColumnPickerChoice[] = [
]; ];
const props: ColumnPickerProps = { const props: ColumnPickerProps = {
columns, availableColumns,
defaultColumns: [1, 3].map(index => columns[index].value), defaultColumns: [1, 3].map(index => availableColumns[index].value),
initialColumns: [1, 3, 4, 6].map(index => columns[index].value), initialColumns: [1, 3, 4, 6].map(index => availableColumns[index].value),
initialOpen: true, initialOpen: true,
onSave: () => undefined hasMore: false,
onFetchMore: () => undefined,
loading: false,
onSave: () => undefined,
query: "",
onQueryChange: () => undefined
}; };
storiesOf("Generics / Column picker", module) storiesOf("Generics / Column picker", module)
@ -41,6 +46,4 @@ storiesOf("Generics / Column picker", module)
.addDecorator(CardDecorator) .addDecorator(CardDecorator)
.addDecorator(Decorator) .addDecorator(Decorator)
.add("default", () => <ColumnPicker {...props} />) .add("default", () => <ColumnPicker {...props} />)
.add("loading", () => ( .add("loading", () => <ColumnPicker {...props} loading hasMore />);
<ColumnPicker {...props} hasMore={true} onFetchMore={() => undefined} />
));

View file

@ -39,6 +39,8 @@ const props: ProductListPageProps = {
activeAttributeSortId: undefined, activeAttributeSortId: undefined,
availableInGridAttributes: attributes, availableInGridAttributes: attributes,
channelsCount: 6, channelsCount: 6,
columnQuery: "",
onColumnQueryChange: () => undefined,
currencySymbol: "USD", currencySymbol: "USD",
defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST], defaultSettings: defaultListSettings[ListViews.PRODUCT_LIST],
filterOpts: productListFilterOpts, filterOpts: productListFilterOpts,

View file

@ -60,6 +60,7 @@ function useAttributeValueSearchHandler(
}, [state.id]); }, [state.id]);
return { return {
query: state.query,
loadMore, loadMore,
search: handleSearch, search: handleSearch,
reset, reset,