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", "context": "WarehouseSettings pickup title",
"string": "Pickup" "string": "Pickup"
}, },
"MJ2jZQ": {
"string": "Total"
},
"MJBAqv": { "MJBAqv": {
"context": "column title used by/customer", "context": "column title used by/customer",
"string": "Used by" "string": "Used by"
@ -3522,6 +3525,9 @@
"context": "channels section name", "context": "channels section name",
"string": "Channels" "string": "Channels"
}, },
"NmK6zy": {
"string": "Payment"
},
"NnhrxZ": { "NnhrxZ": {
"context": "amount of sent refund for transaction", "context": "amount of sent refund for transaction",
"string": "{transactionType} refund" "string": "{transactionType} refund"
@ -4748,10 +4754,6 @@
"context": "dialog content", "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?}}" "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": { "WS4ov0": {
"context": "notifier message", "context": "notifier message",
"string": "Note was added sucessfully" "string": "Note was added sucessfully"
@ -4922,6 +4924,9 @@
"context": "Transaction void button - return preauthorized amount to client", "context": "Transaction void button - return preauthorized amount to client",
"string": "Void" "string": "Void"
}, },
"XPruqs": {
"string": "Order"
},
"XQBVEJ": { "XQBVEJ": {
"context": "order return error description when cannot refund", "context": "order return error description when cannot refund",
"string": "Weve encountered a problem while refunding the products. Products were not refunded. Please try again." "string": "Weve encountered a problem while refunding the products. Products were not refunded. Please try again."
@ -6723,6 +6728,9 @@
"lLwtgs": { "lLwtgs": {
"string": "Variants are disabled in this product type" "string": "Variants are disabled in this product type"
}, },
"lNZuWl": {
"string": "All orders"
},
"lOMgms": { "lOMgms": {
"context": "fulfillment group", "context": "fulfillment group",
"string": "Fulfilled from:" "string": "Fulfilled from:"
@ -6795,6 +6803,9 @@
"context": "deleted multiple attributes", "context": "deleted multiple attributes",
"string": "Attributes successfully delete" "string": "Attributes successfully delete"
}, },
"lwjzVj": {
"string": "Edit order"
},
"lzdvwp": { "lzdvwp": {
"context": "field is optional", "context": "field is optional",
"string": "Optional" "string": "Optional"
@ -7959,6 +7970,9 @@
"un+VWt": { "un+VWt": {
"string": "Search products" "string": "Search products"
}, },
"uoKAmI": {
"string": "Add new order"
},
"usSkzP": { "usSkzP": {
"context": "navigator order mode description", "context": "navigator order mode description",
"string": "Search Orders" "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/paragraph": "^2.8.0",
"@editorjs/quote": "^2.4.0", "@editorjs/quote": "^2.4.0",
"@glideapps/glide-data-grid": "^5.0.0", "@glideapps/glide-data-grid": "^5.0.0",
"@glideapps/glide-data-grid-cells": "^5.2.1",
"@graphiql/plugin-explorer": "^0.1.12", "@graphiql/plugin-explorer": "^0.1.12",
"@graphiql/react": "^0.15.0", "@graphiql/react": "^0.15.0",
"@graphiql/toolkit": "^0.8.0", "@graphiql/toolkit": "^0.8.0",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,8 @@
import { import {
CustomCell, CustomCell,
CustomCellRenderer, CustomRenderer,
getMiddleCenterBias, getMiddleCenterBias,
GridCellKind,
TextCellEntry, TextCellEntry,
} from "@glideapps/glide-data-grid"; } from "@glideapps/glide-data-grid";
import React from "react"; import React from "react";
@ -14,7 +15,8 @@ export interface ThumbnailCellProps {
export type ThumbnailCell = CustomCell<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 => isMatch: (cell: CustomCell): cell is ThumbnailCell =>
(cell.data as any).kind === "thumbnail-cell", (cell.data as any).kind === "thumbnail-cell",
draw: (args, 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 { export function booleanCell(value: boolean): GridCell {
return { return {
...common, ...common,
@ -51,8 +69,12 @@ export function booleanCell(value: boolean): GridCell {
export function loadingCell(): GridCell { export function loadingCell(): GridCell {
return { return {
kind: GridCellKind.Loading, kind: GridCellKind.Custom,
allowOverlay: true, 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 { return {
...common, ...common,
...opts,
kind: GridCellKind.Custom, kind: GridCellKind.Custom,
data: { data: {
kind: "money-cell", kind: "money-cell",

View file

@ -1,5 +1,5 @@
import useLocale from "@dashboard/hooks/useLocale"; 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 { useMemo } from "react";
import { dropdownCellRenderer } from "./DropdownCell"; import { dropdownCellRenderer } from "./DropdownCell";
@ -7,19 +7,20 @@ import { moneyCellRenderer } from "./MoneyCell";
import { numberCellRenderer } from "./NumberCell"; import { numberCellRenderer } from "./NumberCell";
import { thumbnailCellRenderer } from "./ThumbnailCell"; import { thumbnailCellRenderer } from "./ThumbnailCell";
function useCells() { export function useCustomCellRenderers() {
const { locale } = useLocale(); const { locale } = useLocale();
const value = useMemo( const { customRenderers } = useExtraCells();
const renderers = useMemo(
() => [ () => [
moneyCellRenderer(), moneyCellRenderer(),
numberCellRenderer(locale), numberCellRenderer(locale),
dropdownCellRenderer, dropdownCellRenderer,
thumbnailCellRenderer, 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( const onColumnMoved = useCallback(
(startIndex: number, endIndex: number): void => { (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 => setDisplayedColumns(old =>
addAtIndex(old[startIndex], removeAtIndex(old, startIndex), endIndex), addAtIndex(old[startIndex], removeAtIndex(old, startIndex), endIndex),
); );
}, },
[setDisplayedColumns], [availableColumns, setDisplayedColumns],
); );
const onColumnResize = useCallback( const onColumnResize = useCallback(
(column: GridColumn, newSize: number) => (column: GridColumn, newSize: number) =>
@ -34,13 +52,22 @@ export function useColumnsDefault(
[setColumnState], [setColumnState],
); );
const onColumnsChange = useCallback( 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 => [ setDisplayedColumns(prevColumns => [
...emptyColumn,
...(isEmptyColumn
? [availableColumns[1].id]
: [availableColumns[0].id]),
...prevColumns.filter(column => picked.includes(column)), ...prevColumns.filter(column => picked.includes(column)),
...picked ...picked
.filter(column => !prevColumns.find(c => c === column)) .filter(column => !prevColumns.find(c => c === column))
.map(column => availableColumns.find(ac => ac.id === column).id), .map(column => availableColumns.find(ac => ac.id === column).id),
]), ]);
},
[availableColumns, setDisplayedColumns], [availableColumns, setDisplayedColumns],
); );
@ -48,9 +75,10 @@ export function useColumnsDefault(
() => displayedColumns.map(id => columnState.find(ac => ac.id === id)), () => displayedColumns.map(id => columnState.find(ac => ac.id === id)),
[displayedColumns, columnState], [displayedColumns, columnState],
); );
const columnChoices = useMemo( const columnChoices = useMemo(
() => () =>
columns.map(({ id, title }) => ({ applyFilters(columns).map(({ id, title }) => ({
label: title, label: title,
value: id, value: id,
})), })),
@ -58,14 +86,14 @@ export function useColumnsDefault(
); );
const availableColumnsChoices = useMemo( const availableColumnsChoices = useMemo(
() => () =>
availableColumns.map(({ id, title }) => ({ applyFilters(availableColumns).map(({ id, title }) => ({
label: title, label: title,
value: id, value: id,
})), })),
[availableColumns], [availableColumns],
); );
const defaultColumns = useMemo( const defaultColumns = useMemo(
() => availableColumns.map(({ id }) => id), () => applyFilters(availableColumns).map(({ id }) => id),
[availableColumns], [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 = () => { export const useEmptyColumn = () => {
const { theme: currentTheme } = useTheme(); const { themeValues } = useTheme();
const theme = themes[currentTheme];
return { return {
id: "empty", id: "empty",
title: "", title: "",
width: 20, width: 20,
themeOverride: { themeOverride: {
accentColor: theme.colors.background.plain, accentColor: themeValues.colors.background.plain,
accentLight: theme.colors.background.plain, accentLight: themeValues.colors.background.plain,
bgHeaderHovered: theme.colors.background.plain, bgHeaderHovered: themeValues.colors.background.plain,
}, },
}; };
}; };

View file

@ -31,12 +31,10 @@ export const Money: React.FC<MoneyProps> = props => {
return null; return null;
} }
const amount = formatMoneyAmount(money, locale);
return ( return (
<span className={classes.root} {...rest}> <span className={classes.root} {...rest}>
<span className={classes.currency}>{money.currency}</span> <span className={classes.currency}>{money.currency}</span>
{amount} {formatMoneyAmount(money, locale)}
</span> </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 { Grow, Paper, Popper, Typography } from "@material-ui/core";
import { Pill } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { Pill } from "../Pill";
import messages from "./messages"; import messages from "./messages";
import useStyles from "./styles"; import useStyles from "./styles";

View file

@ -1,6 +1,7 @@
import CardTitle from "@dashboard/components/CardTitle"; import CardTitle from "@dashboard/components/CardTitle";
import Grid from "@dashboard/components/Grid"; import Grid from "@dashboard/components/Grid";
import Hr from "@dashboard/components/Hr"; import Hr from "@dashboard/components/Hr";
import { Pill } from "@dashboard/components/Pill";
import { import {
WebhookEventTypeAsyncEnum, WebhookEventTypeAsyncEnum,
WebhookEventTypeSyncEnum, WebhookEventTypeSyncEnum,
@ -17,7 +18,6 @@ import {
PageTab, PageTab,
PageTabPanel, PageTabPanel,
PageTabs, PageTabs,
Pill,
useListWidths, useListWidths,
} from "@saleor/macaw-ui"; } from "@saleor/macaw-ui";
import React, { Dispatch, SetStateAction, useState } from "react"; 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 FormSpacer from "@dashboard/components/FormSpacer";
import Hr from "@dashboard/components/Hr"; import Hr from "@dashboard/components/Hr";
import Link from "@dashboard/components/Link"; import Link from "@dashboard/components/Link";
import { Pill } from "@dashboard/components/Pill";
import { WebhookErrorFragment } from "@dashboard/graphql"; import { WebhookErrorFragment } from "@dashboard/graphql";
import { commonMessages } from "@dashboard/intl"; import { commonMessages } from "@dashboard/intl";
import { getFormErrors } from "@dashboard/utils/errors"; import { getFormErrors } from "@dashboard/utils/errors";
@ -13,7 +14,6 @@ import {
TextField, TextField,
Typography, Typography,
} from "@material-ui/core"; } from "@material-ui/core";
import { Pill } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";

View file

@ -1,5 +1,6 @@
import { Button } from "@dashboard/components/Button"; import { Button } from "@dashboard/components/Button";
import CardTitle from "@dashboard/components/CardTitle"; import CardTitle from "@dashboard/components/CardTitle";
import { Pill } from "@dashboard/components/Pill";
import ResponsiveTable from "@dashboard/components/ResponsiveTable"; import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton"; import Skeleton from "@dashboard/components/Skeleton";
import { TableButtonWrapper } from "@dashboard/components/TableButtonWrapper/TableButtonWrapper"; import { TableButtonWrapper } from "@dashboard/components/TableButtonWrapper/TableButtonWrapper";
@ -21,7 +22,7 @@ import {
TableCell, TableCell,
TableHead, TableHead,
} from "@material-ui/core"; } from "@material-ui/core";
import { DeleteIcon, IconButton, Pill } from "@saleor/macaw-ui"; import { DeleteIcon, IconButton } from "@saleor/macaw-ui";
import clsx from "clsx"; import clsx from "clsx";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; 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 CardTitle from "@dashboard/components/CardTitle";
import { DateTime } from "@dashboard/components/Date"; import { DateTime } from "@dashboard/components/Date";
import Money from "@dashboard/components/Money"; import Money from "@dashboard/components/Money";
import { Pill } from "@dashboard/components/Pill";
import ResponsiveTable from "@dashboard/components/ResponsiveTable"; import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton"; import Skeleton from "@dashboard/components/Skeleton";
import TableRowLink from "@dashboard/components/TableRowLink"; import TableRowLink from "@dashboard/components/TableRowLink";
@ -9,7 +10,7 @@ import { CustomerDetailsQuery } from "@dashboard/graphql";
import { orderUrl } from "@dashboard/orders/urls"; import { orderUrl } from "@dashboard/orders/urls";
import { RelayToFlat } from "@dashboard/types"; import { RelayToFlat } from "@dashboard/types";
import { Card, TableBody, TableCell, TableHead } from "@material-ui/core"; 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 React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";

View file

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

View file

@ -8,6 +8,7 @@ import {
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import { Node, SlugNode } from "@dashboard/types"; import { Node, SlugNode } from "@dashboard/types";
import { ConfirmButtonTransitionState, ThemeType } from "@saleor/macaw-ui"; import { ConfirmButtonTransitionState, ThemeType } from "@saleor/macaw-ui";
import { DefaultTheme, ThemeTokensValues } from "@saleor/macaw-ui/next";
import uniqBy from "lodash/uniqBy"; import uniqBy from "lodash/uniqBy";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { IntlShape } from "react-intl"; import { IntlShape } from "react-intl";
@ -559,3 +560,44 @@ export const getByUnmatchingId =
export const findById = <T extends Node>(id: string, list?: T[]) => export const findById = <T extends Node>(id: string, list?: T[]) =>
list?.find(getById(id)); 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 { DateTime } from "@dashboard/components/Date";
import { Pill } from "@dashboard/components/Pill";
import { transformOrderStatus } from "@dashboard/misc"; import { transformOrderStatus } from "@dashboard/misc";
import { OrderSharedType } from "@dashboard/orders/types"; import { OrderSharedType } from "@dashboard/orders/types";
import { Typography } from "@material-ui/core"; import { Typography } from "@material-ui/core";
import { Skeleton } from "@material-ui/lab"; 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 { Box } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";

View file

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

View file

@ -1,5 +1,6 @@
import { DateTime } from "@dashboard/components/Date"; import { DateTime } from "@dashboard/components/Date";
import Money from "@dashboard/components/Money"; import Money from "@dashboard/components/Money";
import { Pill } from "@dashboard/components/Pill";
import ResponsiveTable from "@dashboard/components/ResponsiveTable"; import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton"; import Skeleton from "@dashboard/components/Skeleton";
import TableCellHeader from "@dashboard/components/TableCellHeader"; import TableCellHeader from "@dashboard/components/TableCellHeader";
@ -22,7 +23,7 @@ import {
TableHead, TableHead,
} from "@material-ui/core"; } from "@material-ui/core";
import { CSSProperties } from "@material-ui/styles"; import { CSSProperties } from "@material-ui/styles";
import { makeStyles, Pill } from "@saleor/macaw-ui"; import { makeStyles } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; 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, sort: OrderListUrlSortField.number,
}, },
params: {}, params: {},
currentTab: 0,
hasPresetsChanged: false,
onTabSave: () => undefined,
onTabUpdate: () => undefined,
}; };
storiesOf("Orders / Order list", module) storiesOf("Orders / Order list", module)

View file

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

View file

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

View file

@ -27,6 +27,23 @@ jest.mock("@saleor/macaw-ui", () => ({
ResponsiveTable: jest.fn(() => <></>), 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", () => ({ jest.mock("@dashboard/hooks/useFlags", () => ({
useFlags: jest.fn(() => ({ orderTransactions: { enabled: false } })), useFlags: jest.fn(() => ({ orderTransactions: { enabled: false } })),
})); }));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import Checkbox from "@dashboard/components/Checkbox"; import Checkbox from "@dashboard/components/Checkbox";
import { Pill } from "@dashboard/components/Pill";
import ResponsiveTable from "@dashboard/components/ResponsiveTable"; import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton"; import Skeleton from "@dashboard/components/Skeleton";
import TableCellHeader from "@dashboard/components/TableCellHeader"; 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 { ListActions, ListProps, SortPage } from "@dashboard/types";
import { getArrowDirection } from "@dashboard/utils/sort"; import { getArrowDirection } from "@dashboard/utils/sort";
import { Card, TableBody, TableCell, TableFooter } from "@material-ui/core"; 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 React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,8 @@ import { ExternalAppProvider } from "@dashboard/apps/components/ExternalAppConte
import { Provider as DateProvider } from "@dashboard/components/Date/DateContext"; import { Provider as DateProvider } from "@dashboard/components/Date/DateContext";
import { Locale, RawLocaleProvider } from "@dashboard/components/Locale"; import { Locale, RawLocaleProvider } from "@dashboard/components/Locale";
import { TimezoneProvider } from "@dashboard/components/Timezone"; 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 React from "react";
import { IntlProvider } from "react-intl"; 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")}> <DateProvider value={+new Date("2018-08-07T14:30:44+00:00")}>
<TimezoneProvider value="America/New_York"> <TimezoneProvider value="America/New_York">
<LegacyThemeProvider>
<ThemeProvider> <ThemeProvider>
<ExternalAppProvider>{children}</ExternalAppProvider> <ExternalAppProvider>{children}</ExternalAppProvider>
</ThemeProvider> </ThemeProvider>
</LegacyThemeProvider>
</TimezoneProvider> </TimezoneProvider>
</DateProvider> </DateProvider>
</RawLocaleProvider> </RawLocaleProvider>