Static datagrid in order view (#3276)

* Init datagrid

* Product type and channels on datagrid

* Move get cell content to utils

* Add channels to storybook

* Add product description to datagrid

* Thubnail cell with avatar

* Translations

* Allow add new empty  product in datagrid

* Search product types

* useProductForm wth save  bar and exit modal

* Add description to product fixture

* Add loading placeholder

* Remove old column picker

* Sorting

* Remove old ProductList

* Update filter props

* Add header icons

* Add sort icons to datagrid column

* Show available channels

* Add price  and updatedAt columns

* Fix sorting, only sort on selected columns

* Sort by channel

* Allow delete name and product type

* Fix show not product found

* Extract mssages

* Product datagrid custom column picker

* Column picker in data grid in dirty hack for bug

* fix storybook props

* Restore Datagrid defalt column picker with custom render

* Add sort by attributes

* Use datagrid loading cells

* Fix product searching

* Show attributes before last updated

* Readonly all fields in datagrid

* Fix creating new datagrid row

* Remove add new procut button from datagrid

* Show only active sorted column

* Temp fix for column filter

* Fix column mismatch

* Add comments and spred props to ColumnPicker

* Cleanup

* Update avatar size and styles

* On row click with hover on row styles

* Use new theme

* Change placeholder image

* Draw rounded image with border

* Readonly product datagrid

* Use new theme colors in datagrid

* Add vertical borders control to datagrid

* Add empty column to add padding

* Add coursor to datagrid

* Restore vertical borders, fix cursor pointer

* Add custom freezed column

* Initial tooltip for column

* Move tooltip to datagrid

* Adjust datagrid colors style, add possibility to select column

* Change datagrid selected cells colors

* Fix typo and extract messages

* Base order  datagrid

* Cleanup Datagrid component

* Cleanup and code refactor

* Remove cursor pointer props from readonlyCell

* Use money cell for total column

* Add custom cell renderers and fix types

* Simple tags implemenrtion for status and payment col

* Add colors from theme

* Make tagCell more dynamic

* Refactor Datagrid file structure

* Add loading indicators

* Selecting column without cells in readonly

* Add sort icons to orders list

* Refactor after CR. fix typos

* Change color of selected colum cell on hover

* Improve selected header text contrast

* Move useColumnPickerColumns to hooks dir with tests

* Add less padding to column picker button

* Remove double border top

* Fix cursor pointer for tagCell and moneyCell

* useGetCellContent hook

* On loading show only one row

* Add missing darkmod color for warning tag

* Refactor columns in datagrid

* Add new macaw theme provider to storybook

* Fix  passing props in datagrid

* Trigger deployment

* Fix column picker in products

* useDatagridColumns

* Fix one more time

* Add column picker with default columns

* Change color for selected header change to textBrandDefault

* Remove unused code, move attributes colums as last

* Cleanup useDatagridColumns

* Improve DatagridProps

* Static datagrid for products (#3144)

* Migrate top nav of product list page to new MacawUI (#3290)

* feat: migrate top nav of product page

* feat: add proper deprecation links

---------

Co-authored-by: Michał Droń <dron.official@yahoo.com>

* Remove datagrid card paddding (#3310)

* Implement card view for product list (#3292)

* Add temporary view switcher

* Add basic product tile view

* Bump macaw-ui

* Add ellipsis

* Bump macaw-ui

* Add status dot & fix non-rectangular thumbnails

* Bump macaw-ui

* Add variable size placeholder icon

* Improve loader

* Fix top nav menu key error

* Add pagination

* Add unit tests

* Extract messages

* Extract status color to function

* CR Refactor

* Hold product view state in local storage (#3315)

* Remember view state for product list

* Use util status function for status dots

* Fix for empty column and hover in datagrid for product (#3324)

* Remove datagrid card paddding (#3310)

* Fix for empty column and hover in datagrid for product (#3324)

* Use themeValues from macaw (#3326)

* Upgrade macaw

* Use themeValues

* Use themeValues from macaw (#3326)

* Upgrade macaw

* Use themeValues

* Add empty column from datagrid, improve theme types

* Use theme type from typeof

* Filter empty column from default

* New product header (#3346)

* Extraxt messages

* Remve title left padding

* Fix switching view

* Add margin right to nav button

* Improve view switch

* Update switch view icons

* Add spacing to switch

* Add more space

* Add new filterbar to order list

* Code refactor and tests

* Extract messages

* Write unit tests

* Improve switch component

* Overwrite Pill styles

* Common method to get status color for pills

* Local Pill component POC

* Add ThemeProvider to test wrapper

* Extract messages

* Refactor Pill

* Fix Pill path

* Fix tests mocks

* Remove scrollbar and border bottom

* Add custom border to to datagrid

* Fix borders

* Fix border bottom

* Refactor and cleanup

* Remove not needed selectionActions code

* Move logic code t misc

* Fix scrollbar and zindex datagrid borders

* Fix product tiles condition

* Fix empty column when save column change

* Fix bottom line in layout overlap

* Keep first column in datagrid not removable

* Fix for not existing column

* Add loader over datagrid, fix problem with border top when empty text in variants

* Fix error color and change color in datagrid

* Filter presets select

* Fix delete tab name in modal, change order preset key

* Extract messages and apply cr fixes

* Keep active tab when filter and search change

* Apply filter in useColumnDefault

---------

Co-authored-by: Michał Droń <dron.official@yahoo.com>
Co-authored-by: Krzysztof Żuraw <9116238+krzysztofzuraw@users.noreply.github.com>
Co-authored-by: Michał Droń <droniu@droniu.dev>
This commit is contained in:
Paweł Chyła 2023-04-20 09:52:44 +02:00 committed by GitHub
parent c5f476152d
commit 8adadfb2ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 1948 additions and 903 deletions

View file

@ -3314,6 +3314,9 @@
"context": "WarehouseSettings pickup title",
"string": "Pickup"
},
"MJ2jZQ": {
"string": "Total"
},
"MJBAqv": {
"context": "column title used by/customer",
"string": "Used by"
@ -3522,6 +3525,9 @@
"context": "channels section name",
"string": "Channels"
},
"NmK6zy": {
"string": "Payment"
},
"NnhrxZ": {
"context": "amount of sent refund for transaction",
"string": "{transactionType} refund"
@ -4748,10 +4754,6 @@
"context": "dialog content",
"string": "{counter,plural,one{Are you sure you want to publish this page?} other{Are you sure you want to publish {displayQuantity} pages?}}"
},
"WRkCFt": {
"context": "tab name",
"string": "All Orders"
},
"WS4ov0": {
"context": "notifier message",
"string": "Note was added sucessfully"
@ -4922,6 +4924,9 @@
"context": "Transaction void button - return preauthorized amount to client",
"string": "Void"
},
"XPruqs": {
"string": "Order"
},
"XQBVEJ": {
"context": "order return error description when cannot refund",
"string": "Weve encountered a problem while refunding the products. Products were not refunded. Please try again."
@ -6723,6 +6728,9 @@
"lLwtgs": {
"string": "Variants are disabled in this product type"
},
"lNZuWl": {
"string": "All orders"
},
"lOMgms": {
"context": "fulfillment group",
"string": "Fulfilled from:"
@ -6795,6 +6803,9 @@
"context": "deleted multiple attributes",
"string": "Attributes successfully delete"
},
"lwjzVj": {
"string": "Edit order"
},
"lzdvwp": {
"context": "field is optional",
"string": "Optional"
@ -7959,6 +7970,9 @@
"un+VWt": {
"string": "Search products"
},
"uoKAmI": {
"string": "Add new order"
},
"usSkzP": {
"context": "navigator order mode description",
"string": "Search Orders"

1608
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,7 @@
"@editorjs/paragraph": "^2.8.0",
"@editorjs/quote": "^2.4.0",
"@glideapps/glide-data-grid": "^5.0.0",
"@glideapps/glide-data-grid-cells": "^5.2.1",
"@graphiql/plugin-explorer": "^0.1.12",
"@graphiql/react": "^0.15.0",
"@graphiql/toolkit": "^0.8.0",

View file

@ -9,6 +9,7 @@ import {
getChannelAvailabilityLabel,
} from "@dashboard/components/ChannelsAvailabilityDropdown/utils";
import Checkbox from "@dashboard/components/Checkbox";
import { Pill } from "@dashboard/components/Pill";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
import TableCellHeader from "@dashboard/components/TableCellHeader";
@ -28,7 +29,7 @@ import {
} from "@dashboard/types";
import { getArrowDirection } from "@dashboard/utils/sort";
import { TableBody, TableCell, TableFooter } from "@material-ui/core";
import { makeStyles, Pill } from "@saleor/macaw-ui";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";

View file

@ -1,9 +1,9 @@
import { Card, Popper } from "@material-ui/core";
import { Pill } from "@saleor/macaw-ui";
import React from "react";
import { useIntl } from "react-intl";
import ChannelsAvailabilityMenuContent from "../ChannelsAvailabilityMenuContent";
import { Pill } from "../Pill";
import { messages } from "./messages";
import {
CollectionChannels,
@ -15,16 +15,17 @@ export interface ChannelsAvailabilityDropdownProps {
channels: CollectionChannels[] | null;
}
export const ChannelsAvailabilityDropdown: React.FC<ChannelsAvailabilityDropdownProps> = ({
channels,
}) => {
export const ChannelsAvailabilityDropdown: React.FC<
ChannelsAvailabilityDropdownProps
> = ({ channels }) => {
const intl = useIntl();
const [isPopupOpen, setPopupOpen] = React.useState(false);
const anchor = React.useRef<HTMLDivElement>(null);
const dropdownColor = React.useMemo(() => getDropdownColor(channels), [
channels,
]);
const dropdownColor = React.useMemo(
() => getDropdownColor(channels),
[channels],
);
if (!channels?.length) {
return (

View file

@ -2,11 +2,12 @@ import HorizontalSpacer from "@dashboard/components/HorizontalSpacer";
import { CollectionFragment } from "@dashboard/graphql";
import ScrollableContent from "@dashboard/plugins/components/PluginsList/PluginAvailabilityStatusPopup/ScrollableContent";
import { Typography } from "@material-ui/core";
import { Pill, PillColor } from "@saleor/macaw-ui";
import { PillColor } from "@saleor/macaw-ui";
import React from "react";
import { MessageDescriptor, useIntl } from "react-intl";
import { messages } from "../ChannelsAvailabilityDropdown/messages";
import { Pill } from "../Pill";
import { useStyles } from "./styles";
export interface ChannelsAvailabilityMenuContentProps {

View file

@ -13,8 +13,8 @@ import DataEditor, {
Item,
} from "@glideapps/glide-data-grid";
import { GetRowThemeCallback } from "@glideapps/glide-data-grid/dist/ts/data-grid/data-grid-render";
import { Card, CardContent, Typography } from "@material-ui/core";
import { Box, useTheme } from "@saleor/macaw-ui/next";
import { Card, CardContent, CircularProgress } from "@material-ui/core";
import { Box, Text, useTheme } from "@saleor/macaw-ui/next";
import clsx from "clsx";
import range from "lodash/range";
import React, {
@ -35,7 +35,7 @@ import { FullScreenContainer } from "./components/FullScreenContainer";
import { Header } from "./components/Header";
import { RowActions } from "./components/RowActions";
import { TooltipContainer } from "./components/TooltipContainer";
import useCells from "./customCells/useCells";
import { useCustomCellRenderers } from "./customCells/useCustomCellRenderers";
import { headerIcons } from "./headerIcons";
import useDatagridChange, {
DatagridChange,
@ -75,6 +75,7 @@ export interface DatagridProps {
rows: number;
title?: string;
fullScreenTitle?: string;
loading?: boolean;
selectionActions: (
selection: number[],
actions: MenuItemsActions,
@ -118,6 +119,7 @@ export const Datagrid: React.FC<DatagridProps> = ({
columnSelect = "none",
onColumnMoved,
onColumnResize,
loading,
hasRowHover = false,
...datagridProps
}): ReactElement => {
@ -125,7 +127,8 @@ export const Datagrid: React.FC<DatagridProps> = ({
const { themeValues } = useTheme();
const datagridTheme = useDatagridTheme(readonly);
const editor = useRef<DataEditorRef>();
const cellProps = useCells();
const customRenderers = useCustomCellRenderers();
const { scrolledToRight, scroller } = useScrollRight();
const defualtColumnPickerProps = getDefultColumnPickerProps(
@ -318,6 +321,14 @@ export const Datagrid: React.FC<DatagridProps> = ({
[selection, selectionActions, handleRemoveRows],
);
if (loading) {
return (
<Box display="flex" justifyContent="center" marginY={12}>
<CircularProgress />
</Box>
);
}
return (
<FullScreenContainer
open={isOpen}
@ -366,8 +377,8 @@ export const Datagrid: React.FC<DatagridProps> = ({
margin="auto"
/>
<DataEditor
{...cellProps}
{...datagridProps}
customRenderers={customRenderers}
verticalBorder={verticalBorder}
headerIcons={headerIcons}
theme={datagridTheme}
@ -463,13 +474,8 @@ export const Datagrid: React.FC<DatagridProps> = ({
</div>
</>
) : (
<Box
borderTopStyle="solid"
borderTopWidth={1}
borderColor="neutralHighlight"
paddingY={9}
>
<Typography align="center">{emptyText}</Typography>
<Box padding={9} textAlign="center">
<Text size="small">{emptyText}</Text>
</Box>
)}
</CardContent>

View file

@ -1,7 +1,8 @@
import {
CustomCell,
CustomCellRenderer,
CustomRenderer,
getMiddleCenterBias,
GridCellKind,
ProvideEditorCallback,
} from "@glideapps/glide-data-grid";
import { makeStyles } from "@saleor/macaw-ui";
@ -93,7 +94,8 @@ const DropdownCellEdit: ReturnType<ProvideEditorCallback<DropdownCell>> = ({
);
};
export const dropdownCellRenderer: CustomCellRenderer<DropdownCell> = {
export const dropdownCellRenderer: CustomRenderer<DropdownCell> = {
kind: GridCellKind.Custom,
isMatch: (c): c is DropdownCell => (c.data as any).kind === "dropdown-cell",
draw: (args, cell) => {
const { ctx, theme, rect } = args;

View file

@ -1,7 +1,8 @@
import {
CustomCell,
CustomCellRenderer,
CustomRenderer,
getMiddleCenterBias,
GridCellKind,
ProvideEditorCallback,
} from "@glideapps/glide-data-grid";
import React from "react";
@ -11,7 +12,7 @@ import { usePriceField } from "../../PriceField/usePriceField";
interface MoneyCellProps {
readonly kind: "money-cell";
readonly currency: string;
readonly value: number | null;
readonly value: number | string | null;
}
export type MoneyCell = CustomCell<MoneyCellProps>;
@ -45,7 +46,8 @@ const MoneyCellEdit: ReturnType<ProvideEditorCallback<MoneyCell>> = ({
);
};
export const moneyCellRenderer = (): CustomCellRenderer<MoneyCell> => ({
export const moneyCellRenderer = (): CustomRenderer<MoneyCell> => ({
kind: GridCellKind.Custom,
isMatch: (c): c is MoneyCell => (c.data as any).kind === "money-cell",
draw: (args, cell) => {
const { ctx, theme, rect } = args;

View file

@ -1,7 +1,8 @@
import {
CustomCell,
CustomCellRenderer,
CustomRenderer,
getMiddleCenterBias,
GridCellKind,
ProvideEditorCallback,
} from "@glideapps/glide-data-grid";
import React from "react";
@ -38,7 +39,8 @@ const NumberCellEdit: ReturnType<ProvideEditorCallback<NumberCell>> = ({
export const numberCellRenderer = (
locale: Locale,
): CustomCellRenderer<NumberCell> => ({
): CustomRenderer<NumberCell> => ({
kind: GridCellKind.Custom,
isMatch: (c): c is NumberCell => (c.data as any).kind === "number-cell",
draw: (args, cell) => {
const { ctx, theme, rect } = args;

View file

@ -1,7 +1,8 @@
import {
CustomCell,
CustomCellRenderer,
CustomRenderer,
getMiddleCenterBias,
GridCellKind,
TextCellEntry,
} from "@glideapps/glide-data-grid";
import React from "react";
@ -14,7 +15,8 @@ export interface ThumbnailCellProps {
export type ThumbnailCell = CustomCell<ThumbnailCellProps>;
export const thumbnailCellRenderer: CustomCellRenderer<ThumbnailCell> = {
export const thumbnailCellRenderer: CustomRenderer<ThumbnailCell> = {
kind: GridCellKind.Custom,
isMatch: (cell: CustomCell): cell is ThumbnailCell =>
(cell.data as any).kind === "thumbnail-cell",
draw: (args, cell) => {

View file

@ -40,6 +40,24 @@ export function readonlyTextCell(
};
}
export function tagsCell(
tags: Array<{ tag: string; color: string }>,
selectedTags: string[],
opts?: Partial<GridCell>,
): GridCell {
return {
...opts,
kind: GridCellKind.Custom,
allowOverlay: true,
copyData: selectedTags.join(", "),
data: {
kind: "tags-cell",
possibleTags: tags,
tags: selectedTags,
},
};
}
export function booleanCell(value: boolean): GridCell {
return {
...common,
@ -51,8 +69,12 @@ export function booleanCell(value: boolean): GridCell {
export function loadingCell(): GridCell {
return {
kind: GridCellKind.Loading,
kind: GridCellKind.Custom,
allowOverlay: true,
copyData: "",
data: {
kind: "spinner-cell",
},
};
}
@ -70,9 +92,14 @@ export function numberCell(
};
}
export function moneyCell(value: number | null, currency: string): MoneyCell {
export function moneyCell(
value: number | string | null,
currency: string,
opts?: Partial<GridCell>,
): MoneyCell {
return {
...common,
...opts,
kind: GridCellKind.Custom,
data: {
kind: "money-cell",

View file

@ -1,5 +1,5 @@
import useLocale from "@dashboard/hooks/useLocale";
import { useCustomCells } from "@glideapps/glide-data-grid";
import { useExtraCells } from "@glideapps/glide-data-grid-cells";
import { useMemo } from "react";
import { dropdownCellRenderer } from "./DropdownCell";
@ -7,19 +7,20 @@ import { moneyCellRenderer } from "./MoneyCell";
import { numberCellRenderer } from "./NumberCell";
import { thumbnailCellRenderer } from "./ThumbnailCell";
function useCells() {
export function useCustomCellRenderers() {
const { locale } = useLocale();
const value = useMemo(
const { customRenderers } = useExtraCells();
const renderers = useMemo(
() => [
moneyCellRenderer(),
numberCellRenderer(locale),
dropdownCellRenderer,
thumbnailCellRenderer,
...customRenderers,
],
[locale],
[customRenderers, locale],
);
return useCustomCells(value);
return renderers;
}
export default useCells;

View file

@ -16,11 +16,29 @@ export function useColumnsDefault(
const onColumnMoved = useCallback(
(startIndex: number, endIndex: number): void => {
// When empty column prevent to rearrange it order
if (availableColumns[0]?.id === "empty") {
if (startIndex === 0) {
return setDisplayedColumns(prevColumns => [...prevColumns]);
}
// Keep empty column always at beginning
if (endIndex === 0) {
return setDisplayedColumns(old =>
addAtIndex(
old[startIndex],
removeAtIndex(old, startIndex),
endIndex + 1,
),
);
}
}
setDisplayedColumns(old =>
addAtIndex(old[startIndex], removeAtIndex(old, startIndex), endIndex),
);
},
[setDisplayedColumns],
[availableColumns, setDisplayedColumns],
);
const onColumnResize = useCallback(
(column: GridColumn, newSize: number) =>
@ -34,13 +52,22 @@ export function useColumnsDefault(
[setColumnState],
);
const onColumnsChange = useCallback(
(picked: string[]) =>
(picked: string[]) => {
// Keep empty column at first place
const isEmptyColumn = availableColumns[0]?.id === "empty";
const emptyColumn = isEmptyColumn ? [availableColumns[0].id] : [];
setDisplayedColumns(prevColumns => [
...emptyColumn,
...(isEmptyColumn
? [availableColumns[1].id]
: [availableColumns[0].id]),
...prevColumns.filter(column => picked.includes(column)),
...picked
.filter(column => !prevColumns.find(c => c === column))
.map(column => availableColumns.find(ac => ac.id === column).id),
]),
]);
},
[availableColumns, setDisplayedColumns],
);
@ -48,9 +75,10 @@ export function useColumnsDefault(
() => displayedColumns.map(id => columnState.find(ac => ac.id === id)),
[displayedColumns, columnState],
);
const columnChoices = useMemo(
() =>
columns.map(({ id, title }) => ({
applyFilters(columns).map(({ id, title }) => ({
label: title,
value: id,
})),
@ -58,14 +86,14 @@ export function useColumnsDefault(
);
const availableColumnsChoices = useMemo(
() =>
availableColumns.map(({ id, title }) => ({
applyFilters(availableColumns).map(({ id, title }) => ({
label: title,
value: id,
})),
[availableColumns],
);
const defaultColumns = useMemo(
() => availableColumns.map(({ id }) => id),
() => applyFilters(availableColumns).map(({ id }) => id),
[availableColumns],
);
@ -84,3 +112,23 @@ export function useColumnsDefault(
},
};
}
function applyFilters(columns: readonly AvailableColumn[]) {
return columns.filter(byNoEmptyColumn).filter(byNotFirstColumn);
}
function byNoEmptyColumn(column: AvailableColumn) {
return column.id !== "empty";
}
function byNotFirstColumn(
_: AvailableColumn,
index: number,
array: AvailableColumn[],
) {
if (array.some(col => col.id === "empty")) {
return index > 1;
}
return index > 0;
}

View file

@ -1,17 +1,16 @@
import { themes, useTheme } from "@saleor/macaw-ui/next";
import { useTheme } from "@saleor/macaw-ui/next";
export const useEmptyColumn = () => {
const { theme: currentTheme } = useTheme();
const theme = themes[currentTheme];
const { themeValues } = useTheme();
return {
id: "empty",
title: "",
width: 20,
themeOverride: {
accentColor: theme.colors.background.plain,
accentLight: theme.colors.background.plain,
bgHeaderHovered: theme.colors.background.plain,
accentColor: themeValues.colors.background.plain,
accentLight: themeValues.colors.background.plain,
bgHeaderHovered: themeValues.colors.background.plain,
},
};
};

View file

@ -31,12 +31,10 @@ export const Money: React.FC<MoneyProps> = props => {
return null;
}
const amount = formatMoneyAmount(money, locale);
return (
<span className={classes.root} {...rest}>
<span className={classes.currency}>{money.currency}</span>
{amount}
{formatMoneyAmount(money, locale)}
</span>
);
};

View file

@ -0,0 +1,35 @@
import { getStatusColor } from "@dashboard/misc";
import { makeStyles, Pill as MacawuiPill, PillProps } from "@saleor/macaw-ui";
import { useTheme, vars } from "@saleor/macaw-ui/next";
import clsx from "clsx";
import React from "react";
const useStyles = makeStyles<{
color: PillProps["color"];
}>(
{
pill: {
borderRadius: "32px",
border: "none",
backgroundColor: ({ color }) => `${color} !important`,
},
},
{ name: "Pill" },
);
// Main purpose of this component is to override default Pill component
// from macaw-ui to add custom styles
// TODO: migrate to Pill component from new macaw-ui when it will be ready
export const Pill = ({ color, ...props }: PillProps) => {
const { theme: currentTheme } = useTheme();
const backgroundColor = getStatusColor(color, currentTheme);
const classes = useStyles({
color: backgroundColor.startsWith("#")
? backgroundColor
: vars.colors.background[backgroundColor],
});
return (
<MacawuiPill {...props} className={clsx(classes.pill, props.className)} />
);
};

View file

@ -0,0 +1 @@
export * from "./Pill";

View file

@ -1,8 +1,8 @@
import { Grow, Paper, Popper, Typography } from "@material-ui/core";
import { Pill } from "@saleor/macaw-ui";
import React from "react";
import { useIntl } from "react-intl";
import { Pill } from "../Pill";
import messages from "./messages";
import useStyles from "./styles";

View file

@ -1,6 +1,7 @@
import CardTitle from "@dashboard/components/CardTitle";
import Grid from "@dashboard/components/Grid";
import Hr from "@dashboard/components/Hr";
import { Pill } from "@dashboard/components/Pill";
import {
WebhookEventTypeAsyncEnum,
WebhookEventTypeSyncEnum,
@ -17,7 +18,6 @@ import {
PageTab,
PageTabPanel,
PageTabs,
Pill,
useListWidths,
} from "@saleor/macaw-ui";
import React, { Dispatch, SetStateAction, useState } from "react";

View file

@ -2,6 +2,7 @@ import CardTitle from "@dashboard/components/CardTitle";
import FormSpacer from "@dashboard/components/FormSpacer";
import Hr from "@dashboard/components/Hr";
import Link from "@dashboard/components/Link";
import { Pill } from "@dashboard/components/Pill";
import { WebhookErrorFragment } from "@dashboard/graphql";
import { commonMessages } from "@dashboard/intl";
import { getFormErrors } from "@dashboard/utils/errors";
@ -13,7 +14,6 @@ import {
TextField,
Typography,
} from "@material-ui/core";
import { Pill } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";

View file

@ -1,5 +1,6 @@
import { Button } from "@dashboard/components/Button";
import CardTitle from "@dashboard/components/CardTitle";
import { Pill } from "@dashboard/components/Pill";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
import { TableButtonWrapper } from "@dashboard/components/TableButtonWrapper/TableButtonWrapper";
@ -21,7 +22,7 @@ import {
TableCell,
TableHead,
} from "@material-ui/core";
import { DeleteIcon, IconButton, Pill } from "@saleor/macaw-ui";
import { DeleteIcon, IconButton } from "@saleor/macaw-ui";
import clsx from "clsx";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";

View file

@ -2,6 +2,7 @@ import { Button } from "@dashboard/components/Button";
import CardTitle from "@dashboard/components/CardTitle";
import { DateTime } from "@dashboard/components/Date";
import Money from "@dashboard/components/Money";
import { Pill } from "@dashboard/components/Pill";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
import TableRowLink from "@dashboard/components/TableRowLink";
@ -9,7 +10,7 @@ import { CustomerDetailsQuery } from "@dashboard/graphql";
import { orderUrl } from "@dashboard/orders/urls";
import { RelayToFlat } from "@dashboard/types";
import { Card, TableBody, TableCell, TableHead } from "@material-ui/core";
import { makeStyles, Pill } from "@saleor/macaw-ui";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";

View file

@ -1,21 +1,21 @@
import { Pill } from "@dashboard/components/Pill";
import {
ExtendedGiftCard,
GiftCardBase,
} from "@dashboard/giftCards/GiftCardUpdate/providers/GiftCardDetailsProvider/types";
import { Pill } from "@saleor/macaw-ui";
import React from "react";
import { useIntl } from "react-intl";
import { giftCardUpdatePageHeaderMessages as giftCardStatusChipMessages } from "../../GiftCardUpdate/GiftCardUpdatePageHeader/messages";
interface GiftCardStatusChipProps<
T extends ExtendedGiftCard<GiftCardBase & { isActive: boolean }>
T extends ExtendedGiftCard<GiftCardBase & { isActive: boolean }>,
> {
giftCard: T;
}
function GiftCardStatusChip<
T extends ExtendedGiftCard<GiftCardBase & { isActive: boolean }>
T extends ExtendedGiftCard<GiftCardBase & { isActive: boolean }>,
>({ giftCard }: GiftCardStatusChipProps<T>) {
const { isExpired, isActive } = giftCard;
const intl = useIntl();

View file

@ -8,6 +8,7 @@ import {
} from "@dashboard/graphql";
import { Node, SlugNode } from "@dashboard/types";
import { ConfirmButtonTransitionState, ThemeType } from "@saleor/macaw-ui";
import { DefaultTheme, ThemeTokensValues } from "@saleor/macaw-ui/next";
import uniqBy from "lodash/uniqBy";
import moment from "moment-timezone";
import { IntlShape } from "react-intl";
@ -559,3 +560,44 @@ export const getByUnmatchingId =
export const findById = <T extends Node>(id: string, list?: T[]) =>
list?.find(getById(id));
const COLOR_WARNING = "#FBE5AC";
const COLOR_WARNING_DARK = "#3E2F0A";
type CustomWarningColor = typeof COLOR_WARNING | typeof COLOR_WARNING_DARK;
export const getStatusColor = (
status: "error" | "warning" | "info" | "success" | "generic",
currentTheme?: DefaultTheme,
): keyof ThemeTokensValues["colors"]["background"] | CustomWarningColor => {
switch (status) {
case "error":
return "surfaceCriticalDepressed";
case "info":
return "surfaceBrandDepressed";
case "success":
return "decorativeSurfaceSubdued2";
case "warning":
// TODO: use color from new macaw theme when will be ready
return currentTheme === "defaultDark"
? COLOR_WARNING_DARK
: COLOR_WARNING;
case "generic":
return "surfaceBrandSubdued";
default:
return "surfaceBrandSubdued";
}
};
export const isFirstColumn = (column: number) => [-1, 0].includes(column);
const getAllRemovedRowsBeforeRowIndex = (
rowIndex: number,
removedRowsIndexs: number[],
) => removedRowsIndexs.filter(r => r <= rowIndex);
export const getDatagridRowDataIndex = (
rowIndex: number,
removedRowsIndexs: number[],
) =>
rowIndex +
getAllRemovedRowsBeforeRowIndex(rowIndex, removedRowsIndexs).length;

View file

@ -1,9 +1,10 @@
import { DateTime } from "@dashboard/components/Date";
import { Pill } from "@dashboard/components/Pill";
import { transformOrderStatus } from "@dashboard/misc";
import { OrderSharedType } from "@dashboard/orders/types";
import { Typography } from "@material-ui/core";
import { Skeleton } from "@material-ui/lab";
import { makeStyles, Pill } from "@saleor/macaw-ui";
import { makeStyles } from "@saleor/macaw-ui";
import { Box } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";

View file

@ -3,6 +3,7 @@ import CardTitle from "@dashboard/components/CardTitle";
import EventTime from "@dashboard/components/EventTime";
import Money, { formatMoney } from "@dashboard/components/Money";
import OverflowTooltip from "@dashboard/components/OverflowTooltip";
import { Pill } from "@dashboard/components/Pill";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import { OrderDetailsFragment } from "@dashboard/graphql/transactions";
import useLocale from "@dashboard/hooks/useLocale";
@ -10,7 +11,7 @@ import { buttonMessages } from "@dashboard/intl";
import { getUserInitials, renderCollection } from "@dashboard/misc";
import { orderGrantRefundEditUrl } from "@dashboard/orders/urls";
import { Card, TableCell, TableRow } from "@material-ui/core";
import { Avatar, Pill } from "@saleor/macaw-ui";
import { Avatar } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";

View file

@ -1,5 +1,6 @@
import { DateTime } from "@dashboard/components/Date";
import Money from "@dashboard/components/Money";
import { Pill } from "@dashboard/components/Pill";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
import TableCellHeader from "@dashboard/components/TableCellHeader";
@ -22,7 +23,7 @@ import {
TableHead,
} from "@material-ui/core";
import { CSSProperties } from "@material-ui/styles";
import { makeStyles, Pill } from "@saleor/macaw-ui";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";

View file

@ -0,0 +1,129 @@
import ColumnPicker from "@dashboard/components/ColumnPicker";
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
import { useColumnsDefault } from "@dashboard/components/Datagrid/hooks/useColumnsDefault";
import {
DatagridChangeStateContext,
useDatagridChangeState,
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
import { OrderListQuery } from "@dashboard/graphql";
import { OrderListUrlSortField } from "@dashboard/orders/urls";
import { ListProps, RelayToFlat, SortPage } from "@dashboard/types";
import { Item } from "@glideapps/glide-data-grid";
import { Box } from "@saleor/macaw-ui/next";
import React, { useCallback } from "react";
import { useIntl } from "react-intl";
import { useColumns, useGetCellContent } from "./datagrid";
import { messages } from "./messages";
import { canBeSorted, getColumnNameAndId, getOrdersRowsLength } from "./utils";
interface OrderListDatagridProps
extends ListProps,
SortPage<OrderListUrlSortField> {
orders: RelayToFlat<OrderListQuery["orders"]>;
onRowClick: (id: string) => void;
hasRowHover?: boolean;
}
export const OrderListDatagrid: React.FC<OrderListDatagridProps> = ({
orders,
disabled,
settings,
onUpdateListSettings,
onSort,
sort,
onRowClick,
hasRowHover,
}) => {
const intl = useIntl();
const datagrid = useDatagridChangeState();
const availableColumns = useColumns(sort);
const ordersLength = getOrdersRowsLength(orders, disabled);
const {
availableColumnsChoices,
columnChoices,
columns,
defaultColumns,
onColumnMoved,
onColumnResize,
onColumnsChange,
picker,
} = useColumnsDefault(availableColumns);
const handleHeaderClick = useCallback(
(col: number) => {
const { columnName, columnId } = getColumnNameAndId(columns[col].id);
if (canBeSorted(columnName)) {
onSort(columnName, columnId);
}
},
[columns, onSort],
);
const handleRowClick = useCallback(
([_, row]: Item) => {
const rowData = orders[row];
onRowClick(rowData.id);
},
[onRowClick, orders],
);
const getCellContent = useGetCellContent({
columns,
orders,
});
return (
<Box __marginTop={ordersLength > 0 ? -1 : 0}>
<DatagridChangeStateContext.Provider value={datagrid}>
<Datagrid
readonly
rowMarkers="none"
loading={disabled}
columnSelect="single"
hasRowHover={hasRowHover}
freezeColumns={2}
verticalBorder={col => (col > 1 ? true : false)}
availableColumns={columns}
onHeaderClicked={handleHeaderClick}
emptyText={intl.formatMessage(messages.emptyText)}
getCellContent={getCellContent}
getCellError={() => false}
menuItems={() => []}
rows={getOrdersRowsLength(orders, disabled)}
selectionActions={() => null}
onColumnResize={onColumnResize}
onColumnMoved={onColumnMoved}
renderColumnPicker={defaultProps => (
<ColumnPicker
{...defaultProps}
availableColumns={availableColumnsChoices}
initialColumns={columnChoices}
defaultColumns={defaultColumns}
onSave={onColumnsChange}
hasMore={false}
loading={false}
onFetchMore={() => undefined}
onQueryChange={picker.setQuery}
query={picker.query}
/>
)}
fullScreenTitle={intl.formatMessage(messages.orders)}
onRowClick={handleRowClick}
/>
<Box paddingX={9}>
<TablePaginationWithContext
component="div"
settings={settings}
disabled={disabled}
onUpdateListSettings={onUpdateListSettings}
/>
</Box>
</DatagridChangeStateContext.Provider>
</Box>
);
};

View file

@ -0,0 +1,228 @@
import {
moneyCell,
readonlyTextCell,
tagsCell,
textCell,
} from "@dashboard/components/Datagrid/customCells/cells";
import { GetCellContentOpts } from "@dashboard/components/Datagrid/Datagrid";
import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn";
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
import { Locale } from "@dashboard/components/Locale";
import { formatMoneyAmount } from "@dashboard/components/Money";
import { OrderListQuery } from "@dashboard/graphql";
import useLocale from "@dashboard/hooks/useLocale";
import {
getStatusColor,
isFirstColumn,
transformOrderStatus,
transformPaymentStatus,
} from "@dashboard/misc";
import { OrderListUrlSortField } from "@dashboard/orders/urls";
import { RelayToFlat, Sort } from "@dashboard/types";
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
import { GridCell, Item } from "@glideapps/glide-data-grid";
import {
DefaultTheme,
ThemeTokensValues,
useTheme,
} from "@saleor/macaw-ui/next";
import moment from "moment-timezone";
import { useMemo } from "react";
import { IntlShape, useIntl } from "react-intl";
import { columnsMessages } from "./messages";
export const useColumns = (sort: Sort<OrderListUrlSortField>) => {
const intl = useIntl();
const emptyColumn = useEmptyColumn();
const columns = useMemo(
() => [
emptyColumn,
{
id: "number",
title: intl.formatMessage(columnsMessages.number),
width: 100,
icon: getColumnSortDirectionIcon(sort, OrderListUrlSortField.number),
},
{
id: "date",
title: intl.formatMessage(columnsMessages.date),
width: 200,
icon: getColumnSortDirectionIcon(sort, OrderListUrlSortField.date),
},
{
id: "customer",
title: intl.formatMessage(columnsMessages.customer),
width: 200,
icon: getColumnSortDirectionIcon(sort, OrderListUrlSortField.customer),
},
{
id: "payment",
title: intl.formatMessage(columnsMessages.payment),
width: 200,
icon: getColumnSortDirectionIcon(sort, OrderListUrlSortField.payment),
},
{
id: "status",
title: intl.formatMessage(columnsMessages.status),
width: 200,
icon: getColumnSortDirectionIcon(
sort,
OrderListUrlSortField.fulfillment,
),
},
{
id: "total",
title: intl.formatMessage(columnsMessages.total),
width: 150,
},
],
[emptyColumn, intl, sort],
);
return columns;
};
interface GetCellContentProps {
columns: AvailableColumn[];
orders: RelayToFlat<OrderListQuery["orders"]>;
}
function getDatagridRowDataIndex(row, removeArray) {
return row + removeArray.filter(r => r <= row).length;
}
export const useGetCellContent = ({ columns, orders }: GetCellContentProps) => {
const intl = useIntl();
const { locale } = useLocale();
const { theme: currentTheme, themeValues } = useTheme();
return (
[column, row]: Item,
{ added, removed }: GetCellContentOpts,
): GridCell => {
if (isFirstColumn(column)) {
return readonlyTextCell("");
}
const columnId = columns[column]?.id;
if (!columnId) {
return readonlyTextCell("");
}
const rowData = added.includes(row)
? undefined
: orders[getDatagridRowDataIndex(row, removed)];
switch (columnId) {
case "number":
return readonlyTextCell(rowData.number);
case "date":
return getDateCellContent(locale, rowData);
case "customer":
return getCustomerCellContent(rowData);
case "payment":
return getPaymentCellContent(intl, themeValues, currentTheme, rowData);
case "status":
return getStatusCellContent(intl, themeValues, currentTheme, rowData);
case "total":
return getTotalCellContent(locale, rowData);
default:
return textCell("");
}
};
};
export function getDateCellContent(
locale: Locale,
rowData: RelayToFlat<OrderListQuery["orders"]>[number],
) {
return readonlyTextCell(moment(rowData.created).locale(locale).format("lll"));
}
export function getCustomerCellContent(
rowData: RelayToFlat<OrderListQuery["orders"]>[number],
) {
if (rowData.billingAddress) {
return readonlyTextCell(
`${rowData.billingAddress.firstName} ${rowData.billingAddress.lastName}`,
);
}
if (rowData.userEmail) {
return readonlyTextCell(rowData.userEmail);
}
return readonlyTextCell("-");
}
export function getPaymentCellContent(
intl: IntlShape,
theme: ThemeTokensValues,
currentTheme: DefaultTheme,
rowData: RelayToFlat<OrderListQuery["orders"]>[number],
) {
const paymentStatus = transformPaymentStatus(rowData.paymentStatus, intl);
if (paymentStatus?.status) {
const statusColor = getStatusColor(paymentStatus.status, currentTheme);
return tagsCell(
[
{
tag: paymentStatus.localized,
color: statusColor.startsWith("#")
? statusColor
: theme.colors.background[statusColor],
},
],
[paymentStatus.localized],
{ cursor: "pointer" },
);
}
return readonlyTextCell("-");
}
export function getStatusCellContent(
intl: IntlShape,
theme: ThemeTokensValues,
currentTheme: DefaultTheme,
rowData: RelayToFlat<OrderListQuery["orders"]>[number],
) {
const status = transformOrderStatus(rowData.status, intl);
const statusColor = getStatusColor(status.status, currentTheme);
if (status) {
return tagsCell(
[
{
tag: status.localized,
color: statusColor.startsWith("#")
? statusColor
: theme.colors.background[statusColor],
},
],
[status.localized],
{ cursor: "pointer" },
);
}
return readonlyTextCell("-");
}
export function getTotalCellContent(
locale: Locale,
rowData: RelayToFlat<OrderListQuery["orders"]>[number],
) {
if (rowData?.total?.gross) {
return moneyCell(
formatMoneyAmount(rowData.total.gross, locale),
rowData.total.gross.currency,
{ cursor: "pointer" },
);
}
return readonlyTextCell("-");
}

View file

@ -0,0 +1 @@
export * from "./OrderListDatagrid";

View file

@ -0,0 +1,47 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
emptyText: {
id: "RlfqSV",
defaultMessage: "No orders found",
},
addOrder: {
id: "uoKAmI",
defaultMessage: "Add new order",
},
editOrder: {
defaultMessage: "Edit order",
id: "lwjzVj",
},
orders: {
defaultMessage: "Order",
id: "XPruqs",
},
});
export const columnsMessages = defineMessages({
number: {
id: "kFkPWB",
defaultMessage: "Number",
},
date: {
id: "P7PLVj",
defaultMessage: "Date",
},
customer: {
id: "hkENym",
defaultMessage: "Customer",
},
payment: {
id: "NmK6zy",
defaultMessage: "Payment",
},
status: {
id: "NWxomz",
defaultMessage: "Fulfillment status",
},
total: {
id: "MJ2jZQ",
defaultMessage: "Total",
},
});

View file

@ -0,0 +1,79 @@
import { OrderListQuery } from "@dashboard/graphql";
import { OrderListUrlSortField } from "@dashboard/orders/urls";
import { RelayToFlat } from "@dashboard/types";
import { canBeSorted, getColumnNameAndId, getOrdersRowsLength } from "./utils";
describe("OrderListDatagrid utils", () => {
describe("getOrdersRowsLength", () => {
it("should return 1 when loading", () => {
// Arrange & Act
const rowLength = getOrdersRowsLength([], true);
// Asset
expect(rowLength).toBe(1);
});
it("should return orders length", () => {
// Arrange & Act
const rowLength = getOrdersRowsLength(
[
{} as RelayToFlat<OrderListQuery["orders"]>[number],
{} as RelayToFlat<OrderListQuery["orders"]>[number],
],
false,
);
// Asset
expect(rowLength).toBe(2);
});
it("should return 0 when no orders and no loading", () => {
// Arrange & Act
const rowLength = getOrdersRowsLength([], false);
// Asset
expect(rowLength).toBe(0);
});
});
describe("getColumnNameAndId", () => {
it("should return column name with id when column name included colon", () => {
// Arrange & Act
const rowLength = getColumnNameAndId("attributes:123");
// Asset
expect(rowLength).toEqual({
columnName: "attributes",
columnId: "123",
});
});
it("should return column name whem column name without colon", () => {
// Arrange & Act
const rowLength = getColumnNameAndId("test123");
// Asset
expect(rowLength).toEqual({
columnName: "test123",
});
});
});
describe("canBeSorted", () => {
it.each([
OrderListUrlSortField.number,
OrderListUrlSortField.date,
OrderListUrlSortField.customer,
OrderListUrlSortField.payment,
OrderListUrlSortField.fulfillment,
])(`should return true when sortable field %s`, sortField => {
expect(canBeSorted(sortField)).toBe(true);
});
it("should return false when not sortable field", () => {
expect(canBeSorted(OrderListUrlSortField.total)).toBe(false);
expect(canBeSorted(OrderListUrlSortField.rank)).toBe(false);
});
});
});

View file

@ -0,0 +1,49 @@
import { OrderListQuery } from "@dashboard/graphql";
import { OrderListUrlSortField } from "@dashboard/orders/urls";
import { RelayToFlat } from "@dashboard/types";
export function getOrdersRowsLength(
orders?: RelayToFlat<OrderListQuery["orders"]>,
loading?: boolean,
) {
if (loading) {
return 1;
}
if (orders?.length) {
return orders.length;
}
return 0;
}
export function getColumnNameAndId(column: string): {
columnName: OrderListUrlSortField;
columnId?: string;
} {
if (column.includes(":")) {
const [columnName, columnId] = column.split(":");
return {
columnName: columnName as OrderListUrlSortField,
columnId,
};
}
return {
columnName: column as OrderListUrlSortField,
};
}
export function canBeSorted(sort: OrderListUrlSortField) {
switch (sort) {
case OrderListUrlSortField.number:
case OrderListUrlSortField.date:
case OrderListUrlSortField.customer:
case OrderListUrlSortField.payment:
case OrderListUrlSortField.fulfillment:
return true;
default:
return false;
}
}

View file

@ -80,6 +80,10 @@ const props: OrderListPageProps = {
sort: OrderListUrlSortField.number,
},
params: {},
currentTab: 0,
hasPresetsChanged: false,
onTabSave: () => undefined,
onTabUpdate: () => undefined,
};
storiesOf("Orders / Order list", module)

View file

@ -4,18 +4,22 @@ import {
useExtensions,
} from "@dashboard/apps/hooks/useExtensions";
import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo";
import { ListFilters } from "@dashboard/components/AppLayout/ListFilters";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { ButtonWithSelect } from "@dashboard/components/ButtonWithSelect";
import CardMenu from "@dashboard/components/CardMenu";
import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks";
import FilterBar from "@dashboard/components/FilterBar";
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
import { ListPageLayout } from "@dashboard/components/Layouts";
import { OrderListQuery, RefreshLimitsQuery } from "@dashboard/graphql";
import useNavigator from "@dashboard/hooks/useNavigator";
import { sectionNames } from "@dashboard/intl";
import { orderMessages } from "@dashboard/orders/messages";
import { DevModeQuery } from "@dashboard/orders/queries";
import {
OrderListUrlQueryParams,
OrderListUrlSortField,
orderUrl,
} from "@dashboard/orders/urls";
import { getFilterVariables } from "@dashboard/orders/views/OrderList/filters";
import {
@ -27,11 +31,12 @@ import {
import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
import { Card } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
import { Box, ChevronRightIcon } from "@saleor/macaw-ui/next";
import React, { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import OrderLimitReached from "../OrderLimitReached";
import OrderList from "../OrderList";
import { OrderListDatagrid } from "../OrderListDatagrid";
import {
createFilterStructure,
OrderFilterKeys,
@ -40,13 +45,16 @@ import {
export interface OrderListPageProps
extends PageListProps,
FilterPageProps<OrderFilterKeys, OrderListFilterOpts>,
Omit<FilterPageProps<OrderFilterKeys, OrderListFilterOpts>, "onTabDelete">,
SortPage<OrderListUrlSortField> {
limits: RefreshLimitsQuery["shop"]["limits"];
orders: RelayToFlat<OrderListQuery["orders"]>;
hasPresetsChanged: boolean;
onSettingsOpen: () => void;
onAdd: () => void;
params: OrderListUrlQueryParams;
onTabUpdate: (tabName: string) => void;
onTabDelete: (tabIndex: number) => void;
}
const useStyles = makeStyles(
@ -59,26 +67,30 @@ const useStyles = makeStyles(
);
const OrderListPage: React.FC<OrderListPageProps> = ({
currentTab,
initialSearch,
filterOpts,
limits,
tabs,
onAdd,
onAll,
onSearchChange,
onSettingsOpen,
onFilterChange,
params,
onTabChange,
onTabDelete,
onTabSave,
params,
onTabUpdate,
tabs,
onAll,
currentTab,
hasPresetsChanged,
...listProps
}) => {
const intl = useIntl();
const classes = useStyles({});
const navigate = useNavigator();
const filterStructure = createFilterStructure(intl, filterOpts);
const limitsReached = isLimitReached(limits, "orders");
const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
const { ORDER_OVERVIEW_CREATE, ORDER_OVERVIEW_MORE_ACTIONS } = useExtensions(
extensionMountPoints.ORDER_LIST,
@ -106,7 +118,40 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
return (
<ListPageLayout>
<TopNav title={intl.formatMessage(sectionNames.orders)}>
<TopNav
title={intl.formatMessage(sectionNames.orders)}
withoutBorder
isAlignToRight={false}
>
<Box
__flex={1}
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Box display="flex">
<Box marginX={6} display="flex" alignItems="center">
<ChevronRightIcon />
</Box>
<FilterPresetsSelect
presetsChanged={hasPresetsChanged}
onSelect={onTabChange}
onRemove={onTabDelete}
onUpdate={onTabUpdate}
savedPresets={tabs}
activePreset={currentTab}
onSelectAll={onAll}
onSave={onTabSave}
isOpen={isFilterPresetOpen}
onOpenChange={setFilterPresetOpen}
selectAllLabel={intl.formatMessage(
orderMessages.filterPresetsAll,
)}
/>
</Box>
<Box display="flex" alignItems="center" gap={5}>
{!!onSettingsOpen && (
<CardMenu
className={classes.settings}
@ -158,31 +203,28 @@ const OrderListPage: React.FC<OrderListPageProps> = ({
)}
/>
)}
</Box>
</Box>
</TopNav>
{limitsReached && <OrderLimitReached />}
<Card>
<FilterBar
currentTab={currentTab}
<ListFilters
initialSearch={initialSearch}
onAll={onAll}
onFilterChange={onFilterChange}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
tabs={tabs}
allTabLabel={intl.formatMessage({
id: "WRkCFt",
defaultMessage: "All Orders",
description: "tab name",
})}
filterStructure={filterStructure}
searchPlaceholder={intl.formatMessage({
id: "wTHjt3",
defaultMessage: "Search Orders...",
})}
/>
<OrderList {...listProps} />
<OrderListDatagrid
{...listProps}
hasRowHover={!isFilterPresetOpen}
onRowClick={id => {
navigate(orderUrl(id));
}}
/>
</Card>
</ListPageLayout>
);

View file

@ -3,6 +3,7 @@ import CardTitle from "@dashboard/components/CardTitle";
import HorizontalSpacer from "@dashboard/components/HorizontalSpacer";
import { Hr } from "@dashboard/components/Hr";
import Money from "@dashboard/components/Money";
import { Pill } from "@dashboard/components/Pill";
import Skeleton from "@dashboard/components/Skeleton";
import {
OrderAction,
@ -11,7 +12,6 @@ import {
OrderStatus,
} from "@dashboard/graphql";
import { Card, CardContent } from "@material-ui/core";
import { Pill } from "@saleor/macaw-ui";
import clsx from "clsx";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";

View file

@ -27,6 +27,23 @@ jest.mock("@saleor/macaw-ui", () => ({
ResponsiveTable: jest.fn(() => <></>),
}));
jest.mock("@saleor/macaw-ui/next", () => ({
useTheme: jest.fn(() => () => ({})),
vars: {
colors: {
border: {
naturalPlain: "",
},
background: {
surfaceCriticalDepressed: "",
surfaceBrandDepressed: "",
decorativeSurfaceSubdued2: "",
surfaceBrandSubdued: "",
},
},
},
}));
jest.mock("@dashboard/hooks/useFlags", () => ({
useFlags: jest.fn(() => ({ orderTransactions: { enabled: false } })),
}));

View file

@ -1,6 +1,7 @@
import { Button } from "@dashboard/components/Button";
import CardTitle from "@dashboard/components/CardTitle";
import { Hr } from "@dashboard/components/Hr";
import { Pill } from "@dashboard/components/Pill";
import Skeleton from "@dashboard/components/Skeleton";
import {
OrderAction,
@ -12,7 +13,6 @@ import {
orderSendRefundUrl,
} from "@dashboard/orders/urls";
import { Card, CardContent, Typography } from "@material-ui/core";
import { Pill } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";

View file

@ -1,5 +1,5 @@
import { Pill } from "@dashboard/components/Pill";
import { TransactionEventStatus } from "@dashboard/orders/types";
import { Pill } from "@saleor/macaw-ui";
import React from "react";
import { useIntl } from "react-intl";

View file

@ -13,6 +13,10 @@ export const orderMessages = defineMessages({
defaultMessage: "Order #{orderNumber}",
description: "page header",
},
filterPresetsAll: {
defaultMessage: "All orders",
id: "lNZuWl",
},
});
export const transactionEventTypeMap = defineMessages<TransactionEventType>({

View file

@ -18,14 +18,19 @@ import usePaginator, {
PaginatorContext,
} from "@dashboard/hooks/usePaginator";
import { useSortRedirects } from "@dashboard/hooks/useSortRedirects";
import { getStringOrPlaceholder } from "@dashboard/misc";
import {
getActiveTabIndexAfterTabDelete,
getNextUniqueTabName,
} from "@dashboard/products/views/ProductList/utils";
import { ListViews } from "@dashboard/types";
import { prepareQs } from "@dashboard/utils/filters/qs";
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
import createFilterHandlers from "@dashboard/utils/handlers/filterHandlers";
import createSortHandler from "@dashboard/utils/handlers/sortHandler";
import { mapEdgesToItems, mapNodeToChoice } from "@dashboard/utils/maps";
import { getSortParams } from "@dashboard/utils/sort";
import React from "react";
import { stringify } from "qs";
import React, { useState } from "react";
import { useIntl } from "react-intl";
import OrderListPage from "../../components/OrderListPage/OrderListPage";
@ -39,13 +44,12 @@ import {
} from "../../urls";
import {
deleteFilterTab,
getActiveFilters,
getFilterOpts,
getFilterQueryParam,
getFiltersCurrentTab,
getFilterTabs,
getFilterVariables,
saveFilterTab,
updateFilterTab,
} from "./filters";
import { DEFAULT_SORT_KEY, getSortQueryVariables } from "./sort";
@ -60,6 +64,8 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
ListViews.ORDER_LIST,
);
const [tabIndexToDelete, setTabIndexToDelete] = useState<number | null>(null);
usePaginationReset(orderListUrl, params, settings.rowNumber);
const intl = useIntl();
@ -91,7 +97,8 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
const tabs = getFilterTabs();
const currentTab = getFiltersCurrentTab(params, tabs);
const currentTab =
params.activeTab !== undefined ? parseInt(params.activeTab, 10) : undefined;
const [changeFilters, resetFilters, handleSearchChange] =
createFilterHandlers({
@ -99,6 +106,7 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
getFilterQueryParam,
navigate,
params,
keepActiveTab: true,
});
const [openModal, closeModal] = createDialogActionHandlers<
@ -106,21 +114,62 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
OrderListUrlQueryParams
>(navigate, orderListUrl, params);
const handleTabChange = (tab: number) =>
navigate(
orderListUrl({
activeTab: tab.toString(),
...getFilterTabs()[tab - 1].data,
}),
const hasPresetsChanged = () => {
const activeTab = tabs[currentTab - 1];
const { paresedQs } = prepareQs(location.search);
return (
activeTab?.data !== stringify(paresedQs) &&
location.search !== "" &&
stringify(paresedQs) !== ""
);
};
const handleTabChange = (tab: number) => {
const qs = new URLSearchParams(getFilterTabs()[tab - 1]?.data ?? "");
qs.append("activeTab", tab.toString());
navigate(orderListUrl() + "?" + qs.toString());
};
const handleFilterTabDelete = () => {
deleteFilterTab(currentTab);
deleteFilterTab(tabIndexToDelete);
// When deleting the current tab, navigate to the All orders tab
if (tabIndexToDelete === currentTab) {
navigate(orderListUrl());
} else {
const currentParams = { ...params };
// When deleting a tab that is not the current one, only remove the action param from the query
delete currentParams.action;
// When deleting a tab that is before the current one, decrease the activeTab param by 1
currentParams.activeTab = getActiveTabIndexAfterTabDelete(
currentTab,
tabIndexToDelete,
);
navigate(orderListUrl() + "?" + stringify(currentParams));
}
};
const hanleFilterTabUpdate = (tabName: string) => {
const { paresedQs, activeTab } = prepareQs(location.search);
updateFilterTab(tabName, stringify(paresedQs));
paresedQs.activeTab = activeTab;
navigate(orderListUrl() + "?" + stringify(paresedQs));
};
const handleFilterTabSave = (data: SaveFilterTabDialogFormData) => {
saveFilterTab(data.name, getActiveFilters(params));
const { paresedQs } = prepareQs(location.search);
saveFilterTab(
getNextUniqueTabName(
data.name,
tabs.map(tab => tab.name),
),
stringify(paresedQs),
);
handleTabChange(tabs.length + 1);
};
@ -169,13 +218,18 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
onSearchChange={handleSearchChange}
onFilterChange={changeFilters}
onTabSave={() => openModal("save-search")}
onTabDelete={() => openModal("delete-search")}
onTabDelete={(tabIndex: number) => {
setTabIndexToDelete(tabIndex);
openModal("delete-search");
}}
onTabChange={handleTabChange}
onTabUpdate={hanleFilterTabUpdate}
initialSearch={params.query || ""}
tabs={getFilterTabs().map(tab => tab.name)}
onAll={resetFilters}
onSettingsOpen={() => navigate(orderSettingsPath)}
params={params}
hasPresetsChanged={hasPresetsChanged()}
/>
<SaveFilterTabDialog
open={params.action === "save-search"}
@ -188,7 +242,7 @@ export const OrderList: React.FC<OrderListProps> = ({ params }) => {
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabDelete}
tabName={getStringOrPlaceholder(tabs[currentTab - 1]?.name)}
tabName={tabs[tabIndexToDelete - 1]?.name ?? "..."}
/>
{!noChannel && (
<ChannelPickerDialog

View file

@ -35,7 +35,7 @@ import {
OrderListUrlQueryParams,
} from "../../urls";
export const ORDER_FILTERS_KEY = "orderFilters";
export const ORDER_FILTERS_KEY = "orderFiltersPresets";
export function getFilterOpts(
params: OrderListUrlFilters,
@ -199,8 +199,12 @@ export function getFilterQueryParam(
}
}
export const { deleteFilterTab, getFilterTabs, saveFilterTab } =
createFilterTabUtils<OrderListUrlFilters>(ORDER_FILTERS_KEY);
export const {
deleteFilterTab,
getFilterTabs,
saveFilterTab,
updateFilterTab,
} = createFilterTabUtils<string>(ORDER_FILTERS_KEY);
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
createFilterUtils<OrderListUrlQueryParams, OrderListUrlFilters>({

View file

@ -21,6 +21,5 @@ export function getSortQueryField(sort: OrderListUrlSortField): OrderSortField {
}
}
export const getSortQueryVariables = createGetSortQueryVariables(
getSortQueryField,
);
export const getSortQueryVariables =
createGetSortQueryVariables(getSortQueryField);

View file

@ -1,4 +1,5 @@
import Checkbox from "@dashboard/components/Checkbox";
import { Pill } from "@dashboard/components/Pill";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
import TableCellHeader from "@dashboard/components/TableCellHeader";
@ -11,7 +12,7 @@ import { PageListUrlSortField, pageUrl } from "@dashboard/pages/urls";
import { ListActions, ListProps, SortPage } from "@dashboard/types";
import { getArrowDirection } from "@dashboard/utils/sort";
import { Card, TableBody, TableCell, TableFooter } from "@material-ui/core";
import { makeStyles, Pill } from "@saleor/macaw-ui";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";

View file

@ -1,6 +1,6 @@
import { Pill } from "@dashboard/components/Pill";
import { PluginBaseFragment } from "@dashboard/graphql";
import { isPluginGlobal } from "@dashboard/plugins/views/utils";
import { Pill } from "@saleor/macaw-ui";
import React from "react";
import { useIntl } from "react-intl";

View file

@ -1,7 +1,7 @@
import CardSpacer from "@dashboard/components/CardSpacer";
import { Pill } from "@dashboard/components/Pill";
import { PluginBaseFragment } from "@dashboard/graphql";
import { CardContent, Typography } from "@material-ui/core";
import { Pill } from "@saleor/macaw-ui";
import React from "react";
import { useIntl } from "react-intl";
@ -14,9 +14,9 @@ interface GlobalConfigPluginPopupBodyProps {
plugin: PluginBaseFragment;
}
const GlobalConfigPluginPopupBody: React.FC<GlobalConfigPluginPopupBodyProps> = ({
plugin,
}) => {
const GlobalConfigPluginPopupBody: React.FC<
GlobalConfigPluginPopupBodyProps
> = ({ plugin }) => {
const intl = useIntl();
const { active } = plugin.globalConfiguration;

View file

@ -13,7 +13,6 @@ import {
SearchAvailableInGridAttributesQuery,
} from "@dashboard/graphql";
import useLocale from "@dashboard/hooks/useLocale";
import { buttonMessages } from "@dashboard/intl";
import { ProductListUrlSortField } from "@dashboard/products/urls";
import { canBeSorted } from "@dashboard/products/views/ProductList/sort";
import { useSearchProductTypes } from "@dashboard/searches/useProductTypeSearch";
@ -27,10 +26,9 @@ import {
} from "@dashboard/types";
import { addAtIndex, removeAtIndex } from "@dashboard/utils/lists";
import { GridColumn, Item } from "@glideapps/glide-data-grid";
import { Button } from "@saleor/macaw-ui";
import { Box } from "@saleor/macaw-ui/next";
import React, { useCallback, useMemo } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useIntl } from "react-intl";
import { isAttributeColumnValue } from "../ProductListPage/utils";
import { useColumnPickerColumns } from "./hooks/useColumnPickerColumns";
@ -87,6 +85,7 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
const searchProductType = useSearchProductTypes();
const datagrid = useDatagridChangeState();
const { locale } = useLocale();
const productsLength = getProductRowsLength(disabled, products, disabled);
const gridAttributesFromSettings = useMemo(
() => settings.columns.filter(isAttributeColumnValue),
[settings.columns],
@ -160,14 +159,12 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
gridAttributes,
gridAttributesFromSettings,
selectedChannelId,
loading,
}),
[
columns,
gridAttributes,
gridAttributesFromSettings,
intl,
loading,
locale,
products,
searchProductType,
@ -225,10 +222,11 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
);
return (
<Box __marginTop={-1}>
<Box __marginTop={productsLength > 0 ? -1 : 0}>
<DatagridChangeStateContext.Provider value={datagrid}>
<Datagrid
readonly
loading={loading}
rowMarkers="none"
columnSelect="single"
freezeColumns={2}
@ -243,12 +241,8 @@ export const ProductListDatagrid: React.FC<ProductListDatagridProps> = ({
getCellContent={getCellContent}
getCellError={() => false}
menuItems={() => []}
rows={getProductRowsLength(disabled, products)}
selectionActions={(indexes, { removeRows }) => (
<Button variant="tertiary" onClick={() => removeRows(indexes)}>
<FormattedMessage {...buttonMessages.delete} />
</Button>
)}
rows={productsLength}
selectionActions={() => null}
fullScreenTitle={intl.formatMessage(messages.products)}
onRowClick={handleRowClick}
renderColumnPicker={defaultProps => (

View file

@ -2,7 +2,6 @@ import { messages } from "@dashboard/components/ChannelsAvailabilityDropdown/mes
import { getChannelAvailabilityLabel } from "@dashboard/components/ChannelsAvailabilityDropdown/utils";
import {
dropdownCell,
loadingCell,
readonlyTextCell,
thumbnailCell,
} from "@dashboard/components/Datagrid/customCells/cells";
@ -18,8 +17,10 @@ import { getMoneyRange } from "@dashboard/components/MoneyRange";
import { ProductListColumns } from "@dashboard/config";
import { GridAttributesQuery, ProductListQuery } from "@dashboard/graphql";
import { commonMessages } from "@dashboard/intl";
import { getDatagridRowDataIndex, isFirstColumn } from "@dashboard/misc";
import { ProductListUrlSortField } from "@dashboard/products/urls";
import { RelayToFlat, Sort } from "@dashboard/types";
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
import { Item } from "@glideapps/glide-data-grid";
import moment from "moment-timezone";
import { IntlShape } from "react-intl";
@ -50,7 +51,7 @@ export function getColumns({
id: "name",
title: intl.formatMessage(commonMessages.product),
width: 300,
icon: getColumnSortIconName(sort, ProductListUrlSortField.name),
icon: getColumnSortDirectionIcon(sort, ProductListUrlSortField.name),
},
{
id: "productType",
@ -105,7 +106,7 @@ export function toAttributeColumnData(
width: 200,
icon:
attributeId === activeAttributeSortId &&
getColumnSortIconName(sort, ProductListUrlSortField.attribute),
getColumnSortDirectionIcon(sort, ProductListUrlSortField.attribute),
};
};
}
@ -134,7 +135,6 @@ interface GetCellContentProps {
gridAttributes: RelayToFlat<GridAttributesQuery["grid"]>;
gridAttributesFromSettings: ProductListColumns[];
selectedChannelId?: string;
loading?: boolean;
}
export function createGetCellContent({
@ -144,20 +144,15 @@ export function createGetCellContent({
locale,
products,
selectedChannelId,
loading,
}: GetCellContentProps) {
return (
[column, row]: Item,
{ changes, getChangeIndex, added, removed }: GetCellContentOpts,
) => {
if (column === -1) {
if (isFirstColumn(column)) {
return readonlyTextCell("");
}
if (loading) {
return loadingCell();
}
const columnId = columns[column]?.id;
if (!columnId) {
@ -167,7 +162,7 @@ export function createGetCellContent({
const change = changes.current[getChangeIndex(columnId, row)]?.data;
const rowData = added.includes(row)
? undefined
: products[row + removed.filter(r => r <= row).length];
: products[getDatagridRowDataIndex(row, removed)];
const channel = rowData?.channelListings?.find(
listing => listing.channel.id === selectedChannelId,
@ -364,7 +359,12 @@ export function getColumnMetadata(column: string) {
export function getProductRowsLength(
disabled: boolean,
product?: RelayToFlat<ProductListQuery["products"]>,
loading?: boolean,
) {
if (loading) {
return 1;
}
if (product?.length) {
return product.length;
}

View file

@ -29,7 +29,7 @@ export const ProductListTiles: React.FC<ProductListTilesProps> = ({
const renderContent = useCallback(() => {
if (loading) {
return (
<Box display="flex" justifyContent="center" height="100%" marginY={12}>
<Box display="flex" justifyContent="center" marginY={12}>
<CircularProgress />
</Box>
);

View file

@ -2,7 +2,8 @@ import { ExternalAppProvider } from "@dashboard/apps/components/ExternalAppConte
import { Provider as DateProvider } from "@dashboard/components/Date/DateContext";
import { Locale, RawLocaleProvider } from "@dashboard/components/Locale";
import { TimezoneProvider } from "@dashboard/components/Timezone";
import { ThemeProvider } from "@saleor/macaw-ui";
import { ThemeProvider as LegacyThemeProvider } from "@saleor/macaw-ui";
import { ThemeProvider } from "@saleor/macaw-ui/next";
import React from "react";
import { IntlProvider } from "react-intl";
@ -19,9 +20,11 @@ const Wrapper: React.FC = ({ children }) => (
>
<DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
<TimezoneProvider value="America/New_York">
<LegacyThemeProvider>
<ThemeProvider>
<ExternalAppProvider>{children}</ExternalAppProvider>
</ThemeProvider>
</LegacyThemeProvider>
</TimezoneProvider>
</DateProvider>
</RawLocaleProvider>