From e933d8de2a6cc0a4ed183f565e85cd547e8e3e3e Mon Sep 17 00:00:00 2001 From: Chloe Rice Date: Thu, 15 Aug 2024 12:20:50 -0400 Subject: [PATCH] [IndexFilters] Prototype "add as filter" search input UX [IndexFilters] Remove search from FilterButton [IndexFilters] Add a SearchField subcomponent [IndexFilters] Move search input beside tabs; prototype add as filter UX [IndexFilters] Update stories to change search placeholder tab reference when active tab changes [Storybook][Test Pages] Create a new ProductsPage story for prototype WIP mock saved filter logic (have to fake outside of admin) switch prototype to tabless filterless orders index ignore commas when filtering by query filter Improve order data with David's feedback Implement fully working tabs and filters to flesh out the table [Key dev dependencies] Bumped TS and Storybook packages Ensure special chars like '#' don't break search --- .../FilterButton/FilterButton.module.css | 6 + .../components/FilterButton/FilterButton.tsx | 28 +- .../components/FilterButton/index.ts | 1 + .../components/SearchField/SearchField.tsx | 108 ++ .../components/SearchField/index.ts | 1 + .../SearchField/tests/SearchField.test.tsx | 87 ++ .../RenderPerformanceProfiler.tsx | 3 +- polaris-react/locales/en.json | 14 +- polaris-react/playground/OrdersPage.tsx | 1213 +++++++++++++++++ polaris-react/playground/orders.ts | 666 +++++++++ polaris-react/playground/stories.tsx | 5 +- .../FilterPill/FilterPill.module.css | 8 + .../components/FilterPill/FilterPill.tsx | 26 +- .../components/FiltersBar/FiltersBar.tsx | 23 +- .../IndexFilters/IndexFilters.stories.tsx | 23 +- .../components/IndexFilters/IndexFilters.tsx | 343 +++-- .../FilterButton/FilterButton.module.css | 6 + .../components/FilterButton/FilterButton.tsx | 58 + .../components/FilterButton/index.ts | 1 + .../components/SearchField/SearchField.tsx | 108 ++ .../components/SearchField/index.ts | 1 + .../components/SearchFilterButton/index.ts | 1 - .../IndexFilters/components/index.ts | 3 +- .../IndexFilters/tests/IndexFilters.test.tsx | 22 +- polaris-react/src/types.ts | 12 +- 25 files changed, 2588 insertions(+), 179 deletions(-) create mode 100644 polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.module.css rename polaris-react/src/components/IndexFilters/components/SearchFilterButton/SearchFilterButton.tsx => polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.tsx (55%) create mode 100644 polaris-internal/src/components/IndexFilters/components/FilterButton/index.ts create mode 100644 polaris-internal/src/components/IndexFilters/components/SearchField/SearchField.tsx create mode 100644 polaris-internal/src/components/IndexFilters/components/SearchField/index.ts create mode 100644 polaris-internal/src/components/IndexFilters/components/SearchField/tests/SearchField.test.tsx create mode 100644 polaris-react/playground/OrdersPage.tsx create mode 100644 polaris-react/playground/orders.ts create mode 100644 polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.module.css create mode 100644 polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.tsx create mode 100644 polaris-react/src/components/IndexFilters/components/FilterButton/index.ts create mode 100644 polaris-react/src/components/IndexFilters/components/SearchField/SearchField.tsx create mode 100644 polaris-react/src/components/IndexFilters/components/SearchField/index.ts delete mode 100644 polaris-react/src/components/IndexFilters/components/SearchFilterButton/index.ts diff --git a/polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.module.css b/polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.module.css new file mode 100644 index 00000000000..27ee0f393ef --- /dev/null +++ b/polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.module.css @@ -0,0 +1,6 @@ +.pressed > button { + /* stylelint-disable polaris/conventions/polaris/custom-property-allowed-list -- Filter section activator pressed state */ + background: var(--pc-button-bg_active); + color: var(--pc-button-color_active); + box-shadow: var(--pc-button-box-shadow_active); +} diff --git a/polaris-react/src/components/IndexFilters/components/SearchFilterButton/SearchFilterButton.tsx b/polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.tsx similarity index 55% rename from polaris-react/src/components/IndexFilters/components/SearchFilterButton/SearchFilterButton.tsx rename to polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.tsx index 6d218c6f836..ad8fee6a0c1 100644 --- a/polaris-react/src/components/IndexFilters/components/SearchFilterButton/SearchFilterButton.tsx +++ b/polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.tsx @@ -1,30 +1,33 @@ import React from 'react'; -import type {CSSProperties} from 'react'; -import {SearchIcon, FilterIcon} from '@shopify/polaris-icons'; +import {FilterIcon} from '@shopify/polaris-icons'; +<<<<<<<< HEAD:polaris-react/src/components/IndexFilters/components/SearchFilterButton/SearchFilterButton.tsx import {Icon} from '../../../Icon'; +======== +>>>>>>>> ee64715c9 ([IndexFilters] Prototype "add as filter" search input UX):polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.tsx import {Tooltip} from '../../../Tooltip'; import {Text} from '../../../Text'; -import {InlineStack} from '../../../InlineStack'; import {Button} from '../../../Button'; -export interface SearchFilterButtonProps { +import styles from './FilterButton.module.css'; + +export interface FilterButtonProps { onClick: () => void; label: string; disabled?: boolean; + pressed?: boolean; tooltipContent: string; disclosureZIndexOverride?: number; - hideFilters?: boolean; - hideQueryField?: boolean; - style: CSSProperties; } -export function SearchFilterButton({ +export function FilterButton({ onClick, label, disabled, + pressed, tooltipContent, disclosureZIndexOverride, +<<<<<<<< HEAD:polaris-react/src/components/IndexFilters/components/SearchFilterButton/SearchFilterButton.tsx style, hideFilters, hideQueryField, @@ -35,14 +38,19 @@ export function SearchFilterButton({ {hideFilters ? null : } ); +======== +}: FilterButtonProps) { + const className = pressed ? styles.pressed : undefined; +>>>>>>>> ee64715c9 ([IndexFilters] Prototype "add as filter" search input UX):polaris-internal/src/components/IndexFilters/components/FilterButton/FilterButton.tsx const activator = ( -
+
diff --git a/polaris-internal/src/components/IndexFilters/components/FilterButton/index.ts b/polaris-internal/src/components/IndexFilters/components/FilterButton/index.ts new file mode 100644 index 00000000000..a934688aba3 --- /dev/null +++ b/polaris-internal/src/components/IndexFilters/components/FilterButton/index.ts @@ -0,0 +1 @@ +export {FilterButton} from './FilterButton'; diff --git a/polaris-internal/src/components/IndexFilters/components/SearchField/SearchField.tsx b/polaris-internal/src/components/IndexFilters/components/SearchField/SearchField.tsx new file mode 100644 index 00000000000..7e8b3a99c5f --- /dev/null +++ b/polaris-internal/src/components/IndexFilters/components/SearchField/SearchField.tsx @@ -0,0 +1,108 @@ +import React, {useId, useState} from 'react'; +import {SearchIcon, ReturnIcon} from '@shopify/polaris-icons'; + +import {Box} from '../../../Box'; +import {Icon} from '../../../Icon'; +import {TextField} from '../../../TextField'; +import {useBreakpoints} from '../../../../utilities/breakpoints'; +import {useI18n} from '../../../../utilities/i18n'; +import {InlineStack} from '../../../InlineStack'; + +export interface SearchFieldProps { + focused?: boolean; + value?: string; + placeholder?: string; + disabled?: boolean; + /** Shows a loading spinner to the right of the input */ + loading?: boolean; + onChange: (value: string) => void; + onFocus?: () => void; + onBlur?: () => void; + onClear?: () => void; + onKeyDownEnter?(): void; +} + +export function SearchField({ + focused: forceFocus = false, + value, + placeholder, + disabled, + loading, + onChange, + onClear, + onFocus, + onBlur, + onKeyDownEnter, +}: SearchFieldProps) { + const id = useId(); + const i18n = useI18n(); + const {mdUp} = useBreakpoints(); + const [focused, setFocused] = useState(forceFocus); + + function handleChange(eventValue: string) { + onChange(eventValue ?? value); + } + + function handleClear() { + if (onClear) { + onClear(); + } else { + onChange(''); + } + } + + function handleKeyDown(event: React.KeyboardEvent) { + if (event.key === 'Enter') onKeyDownEnter?.(); + } + + function handleFocus() { + onFocus?.(); + setFocused(true); + } + + function handleBlur() { + onBlur?.(); + setFocused(false); + } + + const addAsFilterText = + value && focused ? ( + + {i18n.translate('Polaris.IndexFilters.SearchField.action.addAsFilter')} + + + + + ) : undefined; + + return ( +
+ : undefined} + focused={focused} + label={ + placeholder ?? + i18n.translate('Polaris.IndexFilters.SearchField.defaultPlaceholder') + } + labelHidden + clearButton + loading={loading} + suffix={addAsFilterText} + /> +
+ ); +} diff --git a/polaris-internal/src/components/IndexFilters/components/SearchField/index.ts b/polaris-internal/src/components/IndexFilters/components/SearchField/index.ts new file mode 100644 index 00000000000..55415ea36b5 --- /dev/null +++ b/polaris-internal/src/components/IndexFilters/components/SearchField/index.ts @@ -0,0 +1 @@ +export {SearchField} from './SearchField'; diff --git a/polaris-internal/src/components/IndexFilters/components/SearchField/tests/SearchField.test.tsx b/polaris-internal/src/components/IndexFilters/components/SearchField/tests/SearchField.test.tsx new file mode 100644 index 00000000000..aa493a23935 --- /dev/null +++ b/polaris-internal/src/components/IndexFilters/components/SearchField/tests/SearchField.test.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import type {ComponentProps} from 'react'; +import {mountWithApp} from 'tests/utilities'; + +import {SearchField} from '..'; +import {TextField} from '../../../../TextField'; + +jest.mock('../../../../../utilities/breakpoints', () => ({ + ...(jest.requireActual('../../../../../utilities/breakpoints') as any), + useBreakpoints: jest.fn(), +})); + +function mockUseBreakpoints(mdUp: boolean) { + const useBreakpoints: jest.Mock = jest.requireMock( + '../../../../../utilities/breakpoints', + ).useBreakpoints; + + useBreakpoints.mockReturnValue({ + mdUp, + }); +} + +describe('SearchField', () => { + const defaultProps: ComponentProps = { + onChange: jest.fn(), + value: 'foo', + placeholder: 'bar', + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockUseBreakpoints(false); + }); + + it('will call onChange when changed', () => { + const props = {...defaultProps}; + const spy = jest.fn(); + const wrapper = mountWithApp(, {}); + + wrapper.act(() => { + wrapper.find(TextField)!.trigger('onChange'); + }); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('will call onChange correctly when clear button clicked', () => { + const props = {...defaultProps}; + const wrapper = mountWithApp(, {}); + + wrapper.act(() => { + wrapper.find(TextField)?.trigger('onClearButtonClick'); + }); + + expect(props.onChange).toHaveBeenCalledWith(''); + }); + + it('will call onFocus', () => { + const props = {...defaultProps, onFocus: jest.fn()}; + const wrapper = mountWithApp(); + + wrapper.act(() => { + wrapper.findAll('input')[0]?.trigger('onFocus'); + }); + + expect(props.onFocus).toHaveBeenCalledTimes(1); + }); + + it('will call onBlur', () => { + const props = {...defaultProps, onBlur: jest.fn()}; + const wrapper = mountWithApp(); + + wrapper.act(() => { + wrapper.findAll('input')[0]?.trigger('onBlur'); + }); + + expect(props.onBlur).toHaveBeenCalledTimes(1); + }); + + it('will pass the placeholder', () => { + const wrapper = mountWithApp(); + + expect(wrapper).toContainReactComponent('input', { + placeholder: defaultProps.placeholder, + }); + }); +}); diff --git a/polaris-react/.storybook/RenderPerformanceProfiler/RenderPerformanceProfiler.tsx b/polaris-react/.storybook/RenderPerformanceProfiler/RenderPerformanceProfiler.tsx index 4f757177afc..279e342bb69 100644 --- a/polaris-react/.storybook/RenderPerformanceProfiler/RenderPerformanceProfiler.tsx +++ b/polaris-react/.storybook/RenderPerformanceProfiler/RenderPerformanceProfiler.tsx @@ -1,4 +1,5 @@ -import React, {PropsWithChildren} from 'react'; +import React from 'react'; +import type {PropsWithChildren} from 'react'; interface Data { id: string; diff --git a/polaris-react/locales/en.json b/polaris-react/locales/en.json index 24ca91713f6..a1cfd25fe12 100644 --- a/polaris-react/locales/en.json +++ b/polaris-react/locales/en.json @@ -177,9 +177,17 @@ "unsavedChanges": "Unsaved changes - {label}" }, "IndexFilters": { - "searchFilterTooltip": "Search and filter", - "searchFilterTooltipWithShortcut": "Search and filter (F)", - "searchFilterAccessibilityLabel": "Search and filter results", + "searchFilterTooltip": "Filter", + "searchFilterTooltipWithShortcut": "Filter (F)", + "searchFilterAccessibilityLabel": "Filter results", + "editSearchFilter": "Edit search filter", + "SearchField": { + "defaultPlaceholder": "Search", + "action": { + "addAsFilter": "add as filter", + "addToFilter": "add to filter" + } + }, "sort": "Sort your results", "addView": "Add a new view", "newView": "Custom search", diff --git a/polaris-react/playground/OrdersPage.tsx b/polaris-react/playground/OrdersPage.tsx new file mode 100644 index 00000000000..47f5b277402 --- /dev/null +++ b/polaris-react/playground/OrdersPage.tsx @@ -0,0 +1,1213 @@ +// @ts-expect-error -- leave me alone +// @ts-nocheck + +import React, {useEffect, useRef, useState} from 'react'; +import { + ChartVerticalIcon, + AppsIcon, + PersonIcon, + DiscountIcon, + HomeIcon, + TargetIcon, + OrderIcon, + ProductIcon, + SettingsIcon, + SearchIcon, +} from '@shopify/polaris-icons'; + +import type { + TabProps, + IndexFiltersProps, + FilterInterface, + AppliedFilterInterface, + BadgeProps, +} from '../src'; +import { + Thumbnail, + Tag, + Avatar, + Box, + Icon, + InlineStack, + KeyboardKey, + ThemeProvider, + useBreakpoints, + Frame, + Layout, + Navigation, + Page, + FooterHelp, + Link, + ChoiceList, + useIndexResourceState, + IndexTable, + IndexFilters, + TextField, + Card, + Button, + useSetIndexFiltersMode, + IndexFiltersMode, + Badge, + Text, +} from '../src'; + +import {orders} from './orders'; +import type {Order} from './orders'; +import styles from './DetailsPage.module.css'; + +export const OrdersPage = { + tags: ['skip-tests'], + render() { + const skipToContentRef = useRef(null); + const [navItemActive, setNavItemActive] = useState('orders'); + + const contextControlMarkup = ( +
+ + + + + +

Spectrally yours

+
+ ); + + // ---- Navigation ---- + const navigationMarkup = ( + + { + setNavItemActive('orders'); + }, + subNavigationItems: [ + { + label: 'All orders', + matches: navItemActive.includes('orders'), + url: '#', + }, + { + url: '#', + label: 'Drafts', + matches: navItemActive === 'drafts', + disabled: true, + }, + { + url: '#', + label: 'Abandoned checkouts', + matches: navItemActive === 'abandoned', + disabled: true, + }, + ], + }, + { + label: 'Products', + icon: ProductIcon, + matches: navItemActive === 'products', + disabled: true, + url: '#', + onClick: () => { + setNavItemActive('products'); + }, + subNavigationItems: [ + { + label: 'All products', + matches: navItemActive.includes('products'), + url: '#', + }, + { + url: '#', + label: 'Inventory', + disabled: true, + matches: navItemActive === 'inventory', + }, + { + url: '#', + label: 'Transfers', + disabled: true, + matches: navItemActive === 'transfers', + }, + ], + }, + { + label: 'Customers', + icon: PersonIcon, + disabled: true, + matches: navItemActive === 'customers', + url: '#', + }, + { + label: 'Analytics', + icon: ChartVerticalIcon, + disabled: true, + matches: navItemActive === 'analytics', + url: '#', + }, + { + label: 'Marketing', + disabled: true, + icon: TargetIcon, + matches: navItemActive === 'marketing', + url: '#', + }, + { + label: 'Discounts', + disabled: true, + icon: DiscountIcon, + matches: navItemActive === 'discounts', + url: '#', + }, + { + label: 'Apps', + disabled: true, + icon: AppsIcon, + matches: navItemActive === 'apps', + url: '#', + }, + ]} + /> + + + + ); + + // ---- Skip to content target ---- + const skipToContentTarget = ( + + + Page content + + + ); + + // ---- Page markup ---- + const pageMarkup = ( + + + {skipToContentTarget} + + + + + + ); + + return ( + } + navigation={navigationMarkup} + skipToContentTarget={skipToContentRef} + > + {pageMarkup} + + + Learn more about{' '} + + fulfilling orders + + + + ); + }, +}; + +const posIcon = ``; + +function Table({orders}: {orders: Order[]}) { + const [isShowing, setIsShowing] = useState(true); + + const resourceName = { + singular: 'order', + plural: 'orders', + }; + + const {selectedResources, allResourcesSelected, handleSelectionChange} = + // @ts-expect-error -- I don't expect an error here, you're doing too much + useIndexResourceState(orders); + + const mapStatusToBadgeProps = (status: string) => { + let tone: BadgeProps['tone']; + let progress: BadgeProps['progress']; + + if (status === 'Partially fulfilled' || status === 'Payment pending') { + tone = 'warning'; + progress = 'partiallyComplete'; + } else if (status === 'Unfulfilled') { + tone = 'attention'; + progress = 'incomplete'; + } else if (status === 'Overdue') { + tone = 'critical'; + } else { + progress = 'complete'; + } + + return {tone, progress}; + }; + + const rowMarkup = orders.map( + ( + { + id, + date, + customer, + channel, + total, + paymentStatus, + fulfillmentStatus, + items, + deliveryMethod, + tags, + }, + index, + ) => ( + + + + {`#${id}`} + + + {date} + {customer} + {channel} + + + {total} + + + + + {paymentStatus.map((status) => ( + + {status} + + ))} + + + + + {fulfillmentStatus.map((status) => ( + + {status} + + ))} + + + {`${items} items`} + {deliveryMethod} + + + {tags.split(',').map((tag) => ( + {tag} + ))} + + + + ), + ); + + return ( + + {rowMarkup} + + ); +} + +type SavedViewFilter = Pick; + +function OrdersIndexTableWithFilters( + props?: Partial & { + withFilteringByDefault?: boolean; + orders: Order[]; + }, +) { + const sortOptions: IndexFiltersProps['sortOptions'] = [ + {label: 'Order', value: 'order asc', directionLabel: 'Ascending'}, + {label: 'Order', value: 'order desc', directionLabel: 'Descending'}, + {label: 'Customer', value: 'customer asc', directionLabel: 'A-Z'}, + {label: 'Customer', value: 'customer desc', directionLabel: 'Z-A'}, + {label: 'Date', value: 'date asc', directionLabel: 'A-Z'}, + {label: 'Date', value: 'date desc', directionLabel: 'Z-A'}, + {label: 'Total', value: 'total asc', directionLabel: 'Ascending'}, + {label: 'Total', value: 'total desc', directionLabel: 'Descending'}, + ]; + + const [viewNames, setViewNames] = useState([ + 'All', + 'Unfulfilled', + 'Unpaid', + 'Open', + 'Archived', + ]); + const [selectedView, setSelectedView] = useState(0); + const [sortSelected, setSortSelected] = useState(['order asc']); + const [queryValue, setQueryValue] = useState(''); + const [status, setStatus] = useState([]); + const [paymentStatus, setPaymentStatus] = useState([]); + const [fulfillmentStatus, setFulfillmentStatus] = useState([]); + const [loading, setLoading] = useState(false); + const [filteredOrders, setFilteredOrders] = useState(orders); + const [savedViewFilters, setSavedViewFilters] = useState( + [ + [], + [ + { + key: 'status', + label: 'Status', + value: ['Open'], + }, + { + key: 'fulfillmentStatus', + label: 'Fulfillment status', + value: ['Unfulfilled', 'Partially fulfilled'], + }, + ], + [ + { + key: 'status', + label: 'Status', + value: ['Open'], + }, + { + key: 'paymentStatus', + label: 'Payment status', + value: ['Payment pending', 'Overdue'], + }, + ], + [ + { + key: 'status', + label: 'Status', + value: ['Open'], + }, + ], + [ + { + key: 'status', + label: 'Status', + value: ['Archived'], + }, + ], + ], + ); + + const {mode, setMode} = useSetIndexFiltersMode( + props?.withFilteringByDefault ? IndexFiltersMode.Filtering : undefined, + ); + + const escapeSpecialChars = (text: string) => { + return text.replace(/[.*+?^${}()#|[\]\\]/g, ' '); + }; + + const getSearchRegex = (input: string) => { + const terms = escapeSpecialChars(input).split(/\s+/); + const regexParts = terms.map((term) => `(?=.*${term})`); + const regexPattern = regexParts.join(''); + return new RegExp(regexPattern, 'i'); + }; + + const hasTextValueMatches = (inputText: string, order: Order) => { + const regex = getSearchRegex(inputText); + const combinedFields = [ + order.id, + order.customer, + order.channel, + order.deliveryMethod, + order.total, + order.tags, + ].join(' '); + + return regex.test(combinedFields); + }; + + const hasArrayValueMatches = ( + orderValue: Set, + filterValue: Set, + ) => { + if (filterValue.size > 0) { + // @ts-expect-error -- It exists + return orderValue.intersection(filterValue).size > 0; + } + + return true; + }; + + const handleFilterOrders = (nextFilters: { + queryValue?: string; + paymentStatus?: string[]; + fulfillmentStatus?: string[]; + status?: string[]; + }) => { + setLoading(true); + const nextQueryValue = + nextFilters.queryValue !== undefined + ? nextFilters.queryValue + : queryValue; + const nextStatus = + nextFilters.status !== undefined ? nextFilters.status : status; + const nextPaymentStatus = + nextFilters.paymentStatus !== undefined + ? nextFilters.paymentStatus + : paymentStatus; + const nextFulfillmentStatus = + nextFilters.fulfillmentStatus !== undefined + ? nextFilters.fulfillmentStatus + : fulfillmentStatus; + + const statusSet = new Set(nextStatus); + const paymentStatusSet = new Set(nextPaymentStatus); + const fulfillmentStatusSet = new Set(nextFulfillmentStatus); + const result = orders.filter((order) => { + const matchesQueryValue = hasTextValueMatches(nextQueryValue, order); + + const matchesStatus = hasArrayValueMatches( + new Set([order.status]), + statusSet, + ); + + const matchesPaymentStatus = hasArrayValueMatches( + new Set(order.paymentStatus), + paymentStatusSet, + ); + + const matchesFulfillmentStatus = hasArrayValueMatches( + new Set(order.fulfillmentStatus), + fulfillmentStatusSet, + ); + + // if ( + // matchesQueryValue && + // matchesPaymentStatus && + // matchesFulfillmentStatus && + // matchesStatus + // ) { + // console.log( + // ` + // nextFilters: `, + // nextFilters, + // ` + // matchesQueryValue: ${matchesQueryValue}, + // matchesPaymentStatus: ${matchesPaymentStatus}, + // matchesFulfillmentStatus: ${matchesFulfillmentStatus}, + // matchesStatus: ${matchesStatus} + // `, + // ); + // } + setLoading(false); + return ( + matchesQueryValue && + matchesPaymentStatus && + matchesFulfillmentStatus && + matchesStatus + ); + }); + + setFilteredOrders(result); + }; + + // Psuedo-loading state transitions + useEffect(() => { + if (queryValue !== '') { + setLoading(true); + } + const timeoutId = setTimeout(() => { + setLoading(false); + }, 750); + return () => clearTimeout(timeoutId); + }, [queryValue]); + + // ---- Filter input event handlers ---- + + const preProcessInput = (input: string) => { + // Insert a space between numbers and letters if they are adjacent + return input + .replace(/(\d)([a-zA-Z])/g, '$1 $2') + .replace(/([a-zA-Z])(\d)/g, '$1 $2'); + }; + + const handleQueryValueChange = (value: string) => { + const processedInput = preProcessInput(value); + setQueryValue(processedInput); + handleFilterOrders({queryValue: processedInput}); + }; + + const handleQueryValueRemove = () => { + setQueryValue(''); + handleFilterOrders({queryValue: ''}); + }; + + const handlePaymentStatusChange = (value: string[]) => { + setPaymentStatus(value); + handleFilterOrders({paymentStatus: value}); + }; + + const handlePaymentStatusRemove = (value: string[]) => { + setPaymentStatus([]); + handleFilterOrders({paymentStatus: []}); + }; + + const handleFulfillmentStatusChange = (value: string[]) => { + setFulfillmentStatus(value); + handleFilterOrders({fulfillmentStatus: value}); + }; + + const handleFulfillmentStatusRemove = (value: string[]) => { + setFulfillmentStatus([]); + handleFilterOrders({fulfillmentStatus: []}); + }; + + const handleStatusChange = (value: string[]) => { + setStatus(value); + handleFilterOrders({status: value}); + }; + + const handleStatusRemove = (value: string[]) => { + setStatus([]); + handleFilterOrders({status: []}); + }; + + function isEmpty(value: string | string[]) { + return Array.isArray(value) ? value.length === 0 : value === ''; + } + + const isUnsaved = ( + value?: string | string[], + savedValue?: string | string[], + ) => { + if (value === undefined) return false; + if (value.length && savedValue === undefined) return true; + + const isArray = Array.isArray(value) && Array.isArray(status); + const isString = + typeof value === 'string' && typeof savedValue === 'string'; + + if (isString) return value !== savedValue; + if (isArray) + return !( + savedValue?.length && + value.length === savedValue.length && + value.every((status) => savedValue.indexOf(status) > -1) + ); + }; + + const handlers = { + queryValue: { + set: setQueryValue, + change: handleQueryValueChange, + remove: handleQueryValueRemove, + emptyValue: '', + label: 'Search', + }, + status: { + set: setStatus, + change: handleStatusChange, + remove: handleStatusRemove, + label: 'Status', + emptyValue: [], + locked: selectedView === 3 || selectedView === 4, + }, + paymentStatus: { + set: setPaymentStatus, + change: handlePaymentStatusChange, + remove: handlePaymentStatusRemove, + label: 'Payment status', + emptyValue: [], + locked: selectedView === 2, + }, + fulfillmentStatus: { + set: setFulfillmentStatus, + change: handleFulfillmentStatusChange, + remove: handleFulfillmentStatusRemove, + label: 'Fulfillment status', + emptyValue: [], + locked: selectedView === 1, + }, + }; + + const handleChangeFilters = (nextFilterValues: { + queryValue?: string; + paymentStatus?: string[]; + fulfillmentStatus?: string[]; + status?: string[]; + }) => { + for (const key in nextFilterValues) { + if (key in handlers) { + handlers[key].set(nextFilterValues[key]); + } + } + + handleFilterOrders(nextFilterValues); + }; + + // ---- Applied filter event handlers ---- + const handleResetToSavedFilters = (view: number) => { + const nextFilters: { + queryValue: string; + paymentStatus: string[]; + fulfillmentStatus: string[]; + status: string[]; + } = { + queryValue: '', + paymentStatus: [], + fulfillmentStatus: [], + status: [], + }; + console.log('VIEW RESETTING TO: ', view); + savedViewFilters[view]?.forEach(({key, value}) => { + nextFilters[key] = value; + }); + + console.log( + `resetting to --- + `, + nextFilters, + ); + + return handleChangeFilters(nextFilters); + }; + + const handleClearFilters = () => { + handleChangeFilters({ + queryValue: '', + paymentStatus: [], + fulfillmentStatus: [], + status: [], + }); + }; + + const getHumanReadableValue = (label: string, value: string | string[]) => { + if (isEmpty(value)) return ''; + if (!Array.isArray(value)) { + return `${label}: ${value}`; + } + + let humanReadableValue: string; + + if (value.length === 1) { + humanReadableValue = value[0]; + } else if (value.length === 2) { + humanReadableValue = `${value[0]} or ${value[1]}`; + } else { + humanReadableValue = value + .map((text, index) => { + return index !== value.length - 1 ? text : `or ${text}`; + }) + .join(', '); + } + + return `${label}: ${humanReadableValue}`; + }; + // ---- + + const filters: FilterInterface[] = [ + { + key: 'paymentStatus', + value: paymentStatus, + label: handlers.paymentStatus.label, + filter: ( + + ), + }, + { + key: 'fulfillmentStatus', + value: fulfillmentStatus, + label: handlers.fulfillmentStatus.label, + filter: ( + + ), + }, + { + key: 'status', + value: status, + label: handlers.status.label, + filter: ( + + ), + }, + ]; + + const appliedFilters: AppliedFilterInterface[] = []; + + Object.entries({ + queryValue, + status, + paymentStatus, + fulfillmentStatus, + }).forEach(([key, value]) => { + if (isEmpty(value)) return; + + const savedValue = savedViewFilters[selectedView]?.find( + (filter) => filter.key === key, + )?.value; + + appliedFilters.push({ + key, + value, + locked: handlers[key].locked, + label: getHumanReadableValue(handlers[key].label, value), + unsavedChanges: selectedView === 0 ? true : isUnsaved(value, savedValue), + onRemove: handlers[key].locked ? undefined : handlers[key].remove, + }); + }); + + const hasUnsavedChanges = appliedFilters.some( + (filter) => filter.unsavedChanges, + ); + + // ---- View event handlers + const sleep = (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); + }; + + const handleSelectView = async (view: number) => { + setQueryValue(''); + setMode(IndexFiltersMode.Default); + setSelectedView(view); + setLoading(true); + handleResetToSavedFilters(view); + await sleep(250); + setLoading(false); + }; + + const handleEditView = (index: number) => () => { + setMode(IndexFiltersMode.Filtering); + }; + + const handleDeleteView = (index: number) => async () => { + const nextViewNames = [...viewNames]; + const nextSavedViewFilters = [...savedViewFilters]; + nextViewNames.splice(index, 1); + nextSavedViewFilters.splice(index, 1); + setSavedViewFilters(nextSavedViewFilters); + setViewNames(nextViewNames); + await sleep(250); + handleClearFilters(); + return true; + }; + + const handleRenameView = (index: number) => async (newName: string) => { + const nextViewNames = [...viewNames]; + nextViewNames[index] = newName; + await sleep(250); + setViewNames(nextViewNames); + return true; + }; + + const handleDuplicateView = (index: number) => async (name: string) => { + setLoading(true); + const duplicateViewIndex = viewNames.length; + const duplicateViewFilters = [...savedViewFilters[index]]; + setSavedViewFilters((filters) => [...filters, duplicateViewFilters]); + const nextAppliedFilters = { + queryValue: '', + status: [], + paymentStatus: [], + fulfillmentStatus: [], + }; + + duplicateViewFilters.forEach(({key, value}) => { + nextAppliedFilters[key] = value; + }); + setViewNames((names) => [...names, name]); + await sleep(250); + setSelectedView(duplicateViewIndex); + setLoading(false); + return true; + }; + + const handleSaveViewFilters = async (index: number) => { + const nextSavedFilters = [...savedViewFilters]; + nextSavedFilters[index] = appliedFilters.map(({key, value, label}) => ({ + key, + value, + label, + locked: false, + })); + + setSavedViewFilters(nextSavedFilters); + await sleep(300); + return true; + }; + + const handleCreateNewView = async (name: string) => { + const newViewIndex = viewNames.length; + setViewNames((names) => [...names, name]); + setSavedViewFilters((filters) => [...filters, []]); + handleClearFilters(); + await sleep(250); + setMode(IndexFiltersMode.Default); + setSelectedView(newViewIndex); + return true; + }; + + const handleSaveViewAs = async (index: number, name: string) => { + setMode(IndexFiltersMode.Default); + setViewNames((names) => [...names, name]); + setSelectedView(index); + const saved = await handleSaveViewFilters(index); + return saved; + }; + + const handleSave = async (name: string) => { + let saved = false; + const index = !name ? selectedView : viewNames.indexOf(name); + setLoading(true); + + if (index < 0) { + saved = await handleSaveViewAs(viewNames.length, name); + } else { + saved = await handleSaveViewFilters(index); + } + + setLoading(false); + return saved; + }; + + const handleCancel = () => { + if (!hasUnsavedChanges) { + console.log('cancelled -- no unsaved changes'); + } else if (selectedView === 0) { + handleClearFilters(); + console.log('cancelled -- clearing all'); + } else { + handleResetToSavedFilters(selectedView); + console.log('cancelled -- resetting to saved'); + } + }; + const tabs: TabProps[] = viewNames.map((name, index) => { + return { + index, + id: `${name}-${index}`, + content: name, + isLocked: index === 0, + actions: + index === 0 + ? [] + : [ + { + type: 'rename', + onPrimaryAction: handleRenameView(index), + }, + { + type: 'duplicate', + onPrimaryAction: handleDuplicateView(index), + }, + { + type: 'edit', + onAction: handleEditView(index), + }, + { + type: 'delete', + onPrimaryAction: handleDeleteView(index), + }, + ], + onAction: () => {}, + }; + }); + + const primaryAction: IndexFiltersProps['primaryAction'] = { + type: selectedView > 4 ? 'save' : 'save-as', + onAction: handleSave, + disabled: !hasUnsavedChanges, + loading: false, + }; + + const cancelAction: IndexFiltersProps['cancelAction'] = { + onAction: handleCancel, + disabled: false, + loading: false, + }; + + const queryPlaceholder = `Searching in ${viewNames[ + selectedView + ].toLowerCase()}`; + + return ( + + + + + ); +} + +function TopBarPlaceholder({onClickMobileMenu}: {onClickMobileMenu?(): void}) { + const {mdUp} = useBreakpoints(); + + const logoMarkup = ( + + ); + + const mobileMenuActivator = ( +
+
+ ); + + const rightSlotMarkup = ( +
+ {mdUp ? logoMarkup : mobileMenuActivator} +
+ ); + + const centerSlotMarkup = ( +
+ + + + +
+ +
+ + Search + +
+ + + K + +
+
+
+
+ ); + + const secondaryMenuMarkup = ( +
+ +
+ ); + + const userMenuMarkup = ( +
+ {mdUp ? ( + + + + Unicorn Trough + + + ) : ( + + )} +
+ ); + + const leftSlotMarkup = ( + + {secondaryMenuMarkup} + {userMenuMarkup} + + ); + + return ( + + + + {rightSlotMarkup} + {centerSlotMarkup} + {leftSlotMarkup} + + + + ); +} diff --git a/polaris-react/playground/orders.ts b/polaris-react/playground/orders.ts new file mode 100644 index 00000000000..95641e658aa --- /dev/null +++ b/polaris-react/playground/orders.ts @@ -0,0 +1,666 @@ +export interface Order { + id: string; + date: string; + customer: string; + channel: string; + total: string; + paymentStatus: string[]; + fulfillmentStatus: string[]; + status: string; + items: string; + deliveryMethod: string; + tags: string; +} + +export const orders: Order[] = [ + { + id: '1053', + date: 'Aug 22, 2024 at 11:11 pm', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$2,051.20', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Unfulfilled'], + items: '8,205', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1052', + date: 'Aug 22, 2024 at 5:13 am', + customer: 'Esmeralda Ernser', + channel: 'TikTok', + total: '$35.58', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Unfulfilled'], + items: '22', + deliveryMethod: 'Express', + status: 'Open', + tags: 'gift wrap', + }, + { + id: '1051', + date: 'Aug 21, 2024 at 9:59 am', + customer: 'Lindsay Gorczany', + channel: 'TikTok', + total: '$79.86', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '319', + deliveryMethod: '2 Day Air', + status: 'Open', + tags: '', + }, + { + id: '1050', + date: 'Aug 20, 2024 at 11:47 am', + customer: 'Brennan Schowalter', + channel: 'TikTok', + total: '$207.24', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Partially fulfilled'], + items: '829', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1049', + date: 'Aug 20, 2024 at 2:56 am', + customer: 'Ryder Glover', + channel: 'TikTok', + total: '$438.15', + paymentStatus: ['Payment pending'], + fulfillmentStatus: ['Partially fulfilled'], + items: '1,753', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1048', + date: 'Aug 20, 2024 at 2:14 am', + customer: 'Dillon Weissnat', + channel: 'TikTok', + total: '$577.10', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Partially fulfilled'], + items: '2,308', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1047', + date: 'Jul 18, 2024 at 6:46 am', + customer: 'Patrick Gerlach', + channel: 'Online Store', + total: '$56.73', + paymentStatus: ['Payment pending', 'Overdue'], + fulfillmentStatus: ['Unfulfilled'], + items: '227', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1046', + date: 'Jul 6, 2024 at 9:31 pm', + customer: 'Melany Sauer', + channel: 'Online Store', + total: '$237.28', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '949', + deliveryMethod: 'Express', + status: 'Open', + tags: '', + }, + { + id: '1045', + date: 'Jul 4, 2024 at 8:26 am', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$527.76', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '2,111', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1044', + date: 'Jun 26, 2024 at 11:04 am', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$555.51', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '556', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1043', + date: 'Jun 21, 2024 at 10:43 pm', + customer: 'Maxine Weimann', + channel: 'Online Store', + total: '$1,413.86', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '2,828', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1042', + date: 'Jun 17, 2024 at 6:32 pm', + customer: 'Erin Torphy', + channel: 'Online Store', + total: '$1,266.79', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '5,067', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1041', + date: 'Jun 14, 2024 at 10:14 pm', + customer: 'Reginald Herzog', + channel: 'Online Store', + total: '$1,786.00', + paymentStatus: ['Refunded'], + fulfillmentStatus: ['Fulfilled'], + items: '7,144', + deliveryMethod: 'Ground', + status: 'Canceled', + tags: 'vip, wholesale, net 60, damaged in shipment', + }, + { + id: '1040', + date: 'Jun 3, 2024 at 6:17 am', + customer: 'Talia Erdman', + channel: 'TikTok', + total: '$38.81', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '49', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1039', + date: 'Apr 21, 2024 at 6:47 pm', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$1,953.14', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '7,813', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1038', + date: 'Feb 17, 2024 at 7:03 am', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$1,868.27', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '7,473', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1037', + date: 'Jan 27, 2024 at 5:31 pm', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$663.45', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '2,654', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1036', + date: 'Jan 11, 2024 at 10:40 am', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$2,080.11', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '8,320', + deliveryMethod: 'Standard', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1035', + date: ' Dec 28, 2023', + customer: 'Alia Simonis', + channel: 'Instagram', + total: '$165.54', + paymentStatus: ['Refunded'], + fulfillmentStatus: ['Unfulfilled'], + items: '662', + deliveryMethod: 'Overnight', + status: 'Canceled', + tags: 'gift, overnight deadline missed ', + }, + { + id: '1034', + date: ' Dec 5, 2023', + customer: 'Dario Krajcik', + channel: 'Online Store', + total: '$865.93', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '3,464', + deliveryMethod: 'Express', + status: 'Open', + tags: '', + }, + { + id: '1033', + date: ' Nov 29, 2023', + customer: 'Maxine Weimann', + channel: 'Online Store', + total: '$3,091.17', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '12,365', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1032', + date: ' Sep 2, 2023', + customer: 'Kenton Luettgen', + channel: 'Online Store', + total: '$957.20', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '3,829', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1031', + date: ' Jul 17, 2023', + customer: 'Reginald Herzog', + channel: 'Online Store', + total: '$3,063.09', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '12,252', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1030', + date: ' Jun 27, 2023', + customer: 'Laverna Daniel', + channel: 'Online Store', + total: '$486.64', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '1,947', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1029', + date: ' Jun 12, 2023', + customer: 'Maxine Weimann', + channel: 'Online Store', + total: '$2,898.38', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '11,594', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1028', + date: ' May 19, 2023', + customer: 'Guy Haley', + channel: 'Instagram', + total: '$361.90', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '1,448', + deliveryMethod: 'Express', + status: 'Open', + tags: '', + }, + { + id: '1027', + date: ' May 8, 2023', + customer: 'Rico Bednar', + channel: 'Instagram', + total: '$3,839.03', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '15,356', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1026', + date: ' May 6, 2023', + customer: 'Aisha Bahringer', + channel: 'Instagram', + total: '$1,666.90', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '6,668', + deliveryMethod: 'Standard', + status: 'Open', + tags: '', + }, + { + id: '1025', + date: ' Apr 14, 2023', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$4,081.40', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '16,326', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1024', + date: ' Mar 24, 2023', + customer: 'Reginald Herzog', + channel: 'Online Store', + total: '$1,326.84', + paymentStatus: ['Refunded'], + fulfillmentStatus: ['Fulfilled'], + items: '5,307', + deliveryMethod: 'Ground', + status: 'Canceled', + tags: 'vip, wholesale, net 60', + }, + { + id: '1023', + date: ' Feb 25, 2023', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$495.99', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '1,984', + deliveryMethod: 'Express', + status: 'Open', + tags: 'wholesale, net 30', + }, + { + id: '1022', + date: ' Feb 25, 2023', + customer: 'Maxine Weimann', + channel: 'Online Store', + total: '$1,197.61', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '4,790', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1021', + date: ' Feb 10, 2023', + customer: 'Rickey Thompson', + channel: 'Online Store', + total: '$1,575.27', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '6,301', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1020', + date: ' Jan 15, 2023', + customer: 'Erin Torphy', + channel: 'Online Store', + total: '$2,194.11', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '8,776', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1019', + date: ' Jan 8, 2023', + customer: 'Maxine Weimann', + channel: 'Online Store', + total: '$4,836.76', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '19,347', + deliveryMethod: 'Ground', + status: 'Open', + tags: 'vip, wholesale, net 60', + }, + { + id: '1018', + date: ' Dec 10, 2022', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$5,183.79', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '20,735', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1017', + date: ' Nov 26, 2022', + customer: 'Kailyn Paucek', + channel: 'Online Store', + total: '$653.15', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '2,613', + deliveryMethod: 'Express', + status: 'Archived', + tags: '', + }, + { + id: '1016', + date: ' Oct 13, 2022', + customer: 'Walton Rowe', + channel: 'Online Store', + total: '$4,840.18', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '19,361', + deliveryMethod: 'Standard', + status: 'Archived', + tags: 'wholesale, net 30', + }, + { + id: '1015', + date: ' Sep 2, 2022', + customer: 'Reginald Herzog', + channel: 'Online Store', + total: '$4,788.04', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '19,152', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1014', + date: ' Aug 1, 2022', + customer: 'Erin Torphy', + channel: 'Online Store', + total: '$5,784.50', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '23,138', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1013', + date: ' Jul 9, 2022', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$4,779.71', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '19,119', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1012', + date: ' Jun 3, 2022', + customer: 'Erin Torphy', + channel: 'Online Store', + total: '$4,488.62', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '17,954', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1011', + date: ' Apr 4, 2022', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$6,031.40', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '24,126', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1010', + date: ' Jan 26, 2022', + customer: 'Rickey Thompson', + channel: 'Online Store', + total: '$2,072.20', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '8,289', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1009', + date: ' Dec 21, 2021', + customer: 'Ian Nikolaus', + channel: 'Online Store', + total: '$5,132.97', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '20,532', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1008', + date: ' Nov 7, 2021', + customer: 'Erin Torphy', + channel: 'Online Store', + total: '$7,152.06', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '28,608', + deliveryMethod: 'Ground', + status: 'Archived', + tags: 'vip, wholesale, net 60', + }, + { + id: '1007', + date: ' Oct 10, 2021', + customer: 'Sunny Harris', + channel: '', + total: '$22.83', + paymentStatus: ['Refunded'], + fulfillmentStatus: ['Unfulfilled'], + items: '91', + deliveryMethod: 'Local delivery', + status: 'Canceled', + tags: '', + }, + { + id: '1006', + date: ' Sep 28, 2021', + customer: 'Domenic Johnston', + channel: '', + total: '$33.88', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '136', + deliveryMethod: 'Local delivery', + status: 'Archived', + tags: '', + }, + { + id: '1005', + date: ' Sep 20, 2021', + customer: 'Cassie Tromp', + channel: '', + total: '$27.22', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '109', + deliveryMethod: 'Pickup', + status: 'Archived', + tags: '', + }, + { + id: '1004', + date: ' Aug 24, 2021', + customer: 'Anika Ankunding', + channel: '', + total: '$91.91', + paymentStatus: ['Paid'], + fulfillmentStatus: ['Fulfilled'], + items: '138', + deliveryMethod: 'Local delivery', + status: 'Archived', + tags: '', + }, +]; diff --git a/polaris-react/playground/stories.tsx b/polaris-react/playground/stories.tsx index 46b0c692d9b..6f31b26f8b1 100644 --- a/polaris-react/playground/stories.tsx +++ b/polaris-react/playground/stories.tsx @@ -1,14 +1,15 @@ import {Playground} from './Playground'; import {KitchenSink} from './KitchenSink'; import {DetailsPage} from './DetailsPage'; +import {OrdersPage} from './OrdersPage'; export default { // eslint-disable-next-line storybook/no-title-property-in-meta - title: 'Playground', + title: 'Test Pages', parameters: { layout: 'fullscreen', chromatic: {disable: true}, }, }; -export {DetailsPage, KitchenSink, Playground}; +export {Playground, DetailsPage, OrdersPage, KitchenSink}; diff --git a/polaris-react/src/components/Filters/components/FilterPill/FilterPill.module.css b/polaris-react/src/components/Filters/components/FilterPill/FilterPill.module.css index 745010abd2f..61a6666e695 100644 --- a/polaris-react/src/components/Filters/components/FilterPill/FilterPill.module.css +++ b/polaris-react/src/components/Filters/components/FilterPill/FilterPill.module.css @@ -87,6 +87,14 @@ @media (--p-breakpoints-md-up) { padding-right: 0; } + + &.locked { + padding-right: calc(var(--p-space-050) + var(--p-space-300)); + + @media (--p-breakpoints-md-up) { + padding-right: calc(var(--p-space-050) + var(--p-space-200)); + } + } } .clearButton { diff --git a/polaris-react/src/components/Filters/components/FilterPill/FilterPill.tsx b/polaris-react/src/components/Filters/components/FilterPill/FilterPill.tsx index 9a19a980191..4b846fe29be 100644 --- a/polaris-react/src/components/Filters/components/FilterPill/FilterPill.tsx +++ b/polaris-react/src/components/Filters/components/FilterPill/FilterPill.tsx @@ -116,6 +116,7 @@ export function FilterPill({ const toggleButtonClassNames = classNames( styles.PlainButton, styles.ToggleButton, + onRemove === undefined && styles.locked, ); const disclosureMarkup = !selected ? ( @@ -145,18 +146,19 @@ export function FilterPill({ ) : null; - const removeFilterButtonMarkup = selected ? ( - -
- -
-
- ) : null; + const removeFilterButtonMarkup = + selected && onRemove !== undefined ? ( + +
+ +
+
+ ) : null; const activator = (
diff --git a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx index 3a5ca335e02..8759a9026d7 100644 --- a/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx +++ b/polaris-react/src/components/Filters/components/FiltersBar/FiltersBar.tsx @@ -194,16 +194,19 @@ export function FiltersBar({ const pinnedFiltersMarkup = pinnedFilters.map( ({key: filterKey, ...pinnedFilter}) => { const appliedFilter = appliedFilters?.find(({key}) => key === filterKey); - const handleFilterPillRemove = () => { - setLocalPinnedFilters((currentLocalPinnedFilters) => - currentLocalPinnedFilters.filter((key) => { - const isMatchedFilters = key === filterKey; - const isPinnedFilterFromProps = pinnedFromPropsKeys.includes(key); - return !isMatchedFilters || isPinnedFilterFromProps; - }), - ); - appliedFilter?.onRemove(filterKey); - }; + const handleFilterPillRemove = appliedFilter?.locked + ? undefined + : () => { + setLocalPinnedFilters((currentLocalPinnedFilters) => + currentLocalPinnedFilters.filter((key) => { + const isMatchedFilters = key === filterKey; + const isPinnedFilterFromProps = + pinnedFromPropsKeys.includes(key); + return !isMatchedFilters || isPinnedFilterFromProps; + }), + ); + appliedFilter?.onRemove?.(filterKey); + }; return ( setQueryValue('')} onSort={setSortSelected} @@ -687,9 +687,10 @@ export const WithPinnedFilters = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()} + `} onQueryChange={handleFiltersQueryChange} - onQueryClear={() => setQueryValue('')} + onQueryClear={handleQueryValueRemove} onSort={setSortSelected} primaryAction={primaryAction} cancelAction={{ @@ -975,7 +976,7 @@ export const WithPrefilledFilters = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} @@ -1275,7 +1276,7 @@ export const WithHiddenFilter = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} @@ -1612,7 +1613,7 @@ export const WithAsyncData = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} @@ -1918,7 +1919,7 @@ export const Disabled = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} @@ -2087,7 +2088,7 @@ export const WithQueryFieldAndFiltersHidden = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue="" - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={() => {}} onQueryClear={() => {}} onSort={setSortSelected} @@ -2336,7 +2337,7 @@ export const WithNoFilters = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={`Searching in ${itemStrings[selected].toLowerCase()}`} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} @@ -2382,7 +2383,7 @@ export const WithNoFilters = { }, }; -export const WithOnlySearchAndSort = { +export const WithSearchAndSortOnly = { render() { const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -2514,7 +2515,7 @@ export const WithOnlySearchAndSort = { sortOptions={sortOptions} sortSelected={sortSelected} queryValue={queryValue} - queryPlaceholder="Searching in all" + queryPlaceholder={'Search'} onQueryChange={handleFiltersQueryChange} onQueryClear={() => setQueryValue('')} onSort={setSortSelected} diff --git a/polaris-react/src/components/IndexFilters/IndexFilters.tsx b/polaris-react/src/components/IndexFilters/IndexFilters.tsx index 86c119f85c3..7bb6a0f326f 100644 --- a/polaris-react/src/components/IndexFilters/IndexFilters.tsx +++ b/polaris-react/src/components/IndexFilters/IndexFilters.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useCallback, useRef} from 'react'; +import React, {useMemo, useEffect, useCallback, useRef, useState} from 'react'; import {Transition} from 'react-transition-group'; import {useI18n} from '../../utilities/i18n'; @@ -12,13 +12,15 @@ import {Filters} from '../Filters'; import type {FiltersProps} from '../Filters'; import {Tabs} from '../Tabs'; import type {TabsProps} from '../Tabs'; +import {TextField} from '../TextField'; import {useBreakpoints} from '../../utilities/breakpoints'; import {useIsSticky} from './hooks'; import { Container, SortButton, - SearchFilterButton, + SearchField, + FilterButton, UpdateButtons, EditColumnsButton, } from './components'; @@ -70,7 +72,7 @@ export interface IndexFiltersProps onSortKeyChange?: (value: string) => void; /** Optional callback when using saved views and changing the sort direction */ onSortDirectionChange?: (value: string) => void; - /** Callback when the add filter button is clicked, to be passed to AlphaFilters. */ + /** Callback when the add filter button is clicked, to be passed to Filters. */ onAddFilterClick?: () => void; /** The primary action to display */ primaryAction?: IndexFiltersPrimaryAction; @@ -156,6 +158,18 @@ export function IndexFilters({ const defaultRef = useRef(null); const filteringRef = useRef(null); + const [searchOnlyValue, setSearchOnlyValue] = useState(''); + const [searchFilterValue, setSearchFilterValue] = useState(queryValue); + + useEffect(() => { + if (queryValue === '') { + setSearchOnlyValue(''); + setSearchFilterValue(''); + } else if (queryValue.length > 0 && searchOnlyValue.length === 0) { + setSearchFilterValue(queryValue); + } + }, [queryValue, searchOnlyValue]); + const { value: filtersFocused, setFalse: setFiltersUnFocused, @@ -203,13 +217,6 @@ export function IndexFilters({ [onSort], ); - const handleChangeSearch = useCallback( - (value: string) => { - onQueryChange(value); - }, - [onQueryChange], - ); - const useExecutedCallback = ( action?: ExecutedCallback, afterEffect?: () => void, @@ -229,6 +236,8 @@ export function IndexFilters({ const onExecutedCancelAction = useCallback(() => { cancelAction?.onAction?.(); + // setSearchOnlyValue(''); + // setSearchFilterValue(''); setMode(IndexFiltersMode.Default); }, [cancelAction, setMode]); @@ -309,8 +318,21 @@ export function IndexFilters({ const isActionLoading = primaryAction?.loading || cancelAction?.loading; - function handleClickFilterButton() { + function handleHideFilters() { + cancelAction?.onAction(); + setMode(IndexFiltersMode.Default); + } + + const handleShowFilters = useCallback(() => { beginEdit(IndexFiltersMode.Filtering); + }, [beginEdit]); + + function handleClickFilterButton() { + if (mode === IndexFiltersMode.Filtering) { + handleHideFilters(); + } else { + handleShowFilters(); + } } const searchFilterTooltipLabelId = disableKeyboardShortcuts @@ -330,9 +352,44 @@ export function IndexFilters({ setMode(IndexFiltersMode.Default); } - function handleClearSearch() { - onQueryClear?.(); - } + const handleQueryChange = useCallback( + (input: 'searchOnly' | 'searchFilter') => (value: string) => { + if (input === 'searchOnly') { + onQueryChange(searchFilterValue ? searchFilterValue + value : value); + setSearchOnlyValue(value); + } else { + onQueryChange(searchOnlyValue ? value + searchOnlyValue : value); + setSearchFilterValue(value); + } + }, + [searchFilterValue, searchOnlyValue, onQueryChange], + ); + + const handleAddAsFilter = useCallback(() => { + if (mode !== IndexFiltersMode.Filtering) { + handleShowFilters(); + } + if (searchOnlyValue) { + setSearchFilterValue((searchFilter) => + searchFilter ? `${searchFilter},${searchOnlyValue}` : searchOnlyValue, + ); + setSearchOnlyValue(''); + } + }, [mode, handleShowFilters, searchOnlyValue]); + + const handleQueryClear = useCallback( + (input: 'searchOnly' | 'searchFilter') => () => { + if (input === 'searchOnly') { + setSearchOnlyValue(''); + onQueryChange(searchFilterValue); + } else { + onQueryClear?.(); + setSearchOnlyValue(''); + setSearchFilterValue(''); + } + }, + [searchFilterValue, onQueryChange, onQueryClear], + ); function handleQueryBlur() { setFiltersUnFocused(); @@ -347,9 +404,88 @@ export function IndexFilters({ if (mode !== IndexFiltersMode.Default) { return; } - beginEdit(IndexFiltersMode.Filtering); + + handleShowFilters(); } + const handleKeyDownEnter = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + onQueryChange( + searchOnlyValue + ? searchFilterValue + searchOnlyValue + : searchFilterValue, + ); + } + }, + [searchOnlyValue, searchFilterValue, onQueryChange], + ); + + const searchFilterLabel = i18n.translate( + 'Polaris.IndexFilters.SearchField.defaultPlaceholder', + ); + + const filtersWithSearch = [ + ...filters, + { + key: 'appliedSearchFilter', + label: searchFilterLabel, + hidden: true, + filter: ( +
+ +
+ ), + }, + ]; + + const getAppliedFilters = useMemo(() => { + let searchFilter; + + const supportsSavedFilters = + !hideFilters && + primaryAction && + (primaryAction.type === 'save' || primaryAction.type === 'save-as'); + + if (queryValue && supportsSavedFilters && searchFilterValue) { + searchFilter = { + key: 'appliedSearchFilter', + label: `${searchFilterLabel}: ${searchFilterValue}`, + value: searchFilterValue, + unsavedChanges: appliedFilters?.find( + ({key}) => key.includes('search') || key.includes('query'), + )?.unsavedChanges, + onRemove: handleQueryClear('searchFilter'), + }; + } + + if (searchFilter) { + return Array.isArray(appliedFilters) + ? [...appliedFilters, searchFilter] + : [searchFilter]; + } + + return appliedFilters; + }, [ + queryValue, + hideFilters, + primaryAction, + appliedFilters, + searchFilterValue, + searchFilterLabel, + handleQueryClear, + ]); + return (
- - {(state) => ( -
- {mode !== IndexFiltersMode.Filtering ? ( - - -
-
- -
- {isLoading && mdDown && ( -
- -
- )} -
-
- {isLoading && !mdDown && ( -
- {isLoading ? : null} -
- )} - {mode === IndexFiltersMode.Default ? ( - <> - {hideFilters && hideQueryField ? null : ( - - )} - {editColumnsMarkup} - {sortMarkup} - - ) : null} - {mode === IndexFiltersMode.EditingColumns - ? updateButtonsMarkup - : null} -
-
-
- ) : null} -
- )} -
+
+ + +
+
+ +
+ {isLoading && mdDown && ( +
+ +
+ )} +
+
+ + {isLoading && !mdDown && ( +
+ {isLoading ? : null} +
+ )} + + {hideFilters ? null : ( + + )} + {editColumnsMarkup} + {sortMarkup} + {mode === IndexFiltersMode.EditingColumns + ? updateButtonsMarkup + : null} +
+
+
+
+ {mode === IndexFiltersMode.Filtering ? ( {}} + onQueryClear={() => {}} onAddFilterClick={onAddFilterClick} - filters={filters} - appliedFilters={appliedFilters} + filters={filtersWithSearch} + appliedFilters={getAppliedFilters} onClearAll={onClearAll} disableFilters={disabled} hideFilters={hideFilters} - hideQueryField={hideQueryField} - disableQueryField={disabled || disableQueryField} loading={loading || isActionLoading} focused={filtersFocused} mountedState={mdDown ? undefined : state} - borderlessQueryField closeOnChildOverlayClick={closeOnChildOverlayClick} >
diff --git a/polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.module.css b/polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.module.css new file mode 100644 index 00000000000..27ee0f393ef --- /dev/null +++ b/polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.module.css @@ -0,0 +1,6 @@ +.pressed > button { + /* stylelint-disable polaris/conventions/polaris/custom-property-allowed-list -- Filter section activator pressed state */ + background: var(--pc-button-bg_active); + color: var(--pc-button-color_active); + box-shadow: var(--pc-button-box-shadow_active); +} diff --git a/polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.tsx b/polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.tsx new file mode 100644 index 00000000000..7178e8d575a --- /dev/null +++ b/polaris-react/src/components/IndexFilters/components/FilterButton/FilterButton.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import {FilterIcon} from '@shopify/polaris-icons'; + +import {Tooltip} from '../../../Tooltip'; +import {Text} from '../../../Text'; +import {Button} from '../../../Button'; + +import styles from './FilterButton.module.css'; + +export interface FilterButtonProps { + onClick: () => void; + label: string; + disabled?: boolean; + pressed?: boolean; + tooltipContent: string; + disclosureZIndexOverride?: number; +} + +export function FilterButton({ + onClick, + label, + disabled, + pressed, + tooltipContent, + disclosureZIndexOverride, +}: FilterButtonProps) { + const className = pressed ? styles.pressed : undefined; + + const activator = ( +
+
+ ); + + const content = ( + + {tooltipContent} + + ); + + return ( + + {activator} + + ); +} diff --git a/polaris-react/src/components/IndexFilters/components/FilterButton/index.ts b/polaris-react/src/components/IndexFilters/components/FilterButton/index.ts new file mode 100644 index 00000000000..a934688aba3 --- /dev/null +++ b/polaris-react/src/components/IndexFilters/components/FilterButton/index.ts @@ -0,0 +1 @@ +export {FilterButton} from './FilterButton'; diff --git a/polaris-react/src/components/IndexFilters/components/SearchField/SearchField.tsx b/polaris-react/src/components/IndexFilters/components/SearchField/SearchField.tsx new file mode 100644 index 00000000000..7e8b3a99c5f --- /dev/null +++ b/polaris-react/src/components/IndexFilters/components/SearchField/SearchField.tsx @@ -0,0 +1,108 @@ +import React, {useId, useState} from 'react'; +import {SearchIcon, ReturnIcon} from '@shopify/polaris-icons'; + +import {Box} from '../../../Box'; +import {Icon} from '../../../Icon'; +import {TextField} from '../../../TextField'; +import {useBreakpoints} from '../../../../utilities/breakpoints'; +import {useI18n} from '../../../../utilities/i18n'; +import {InlineStack} from '../../../InlineStack'; + +export interface SearchFieldProps { + focused?: boolean; + value?: string; + placeholder?: string; + disabled?: boolean; + /** Shows a loading spinner to the right of the input */ + loading?: boolean; + onChange: (value: string) => void; + onFocus?: () => void; + onBlur?: () => void; + onClear?: () => void; + onKeyDownEnter?(): void; +} + +export function SearchField({ + focused: forceFocus = false, + value, + placeholder, + disabled, + loading, + onChange, + onClear, + onFocus, + onBlur, + onKeyDownEnter, +}: SearchFieldProps) { + const id = useId(); + const i18n = useI18n(); + const {mdUp} = useBreakpoints(); + const [focused, setFocused] = useState(forceFocus); + + function handleChange(eventValue: string) { + onChange(eventValue ?? value); + } + + function handleClear() { + if (onClear) { + onClear(); + } else { + onChange(''); + } + } + + function handleKeyDown(event: React.KeyboardEvent) { + if (event.key === 'Enter') onKeyDownEnter?.(); + } + + function handleFocus() { + onFocus?.(); + setFocused(true); + } + + function handleBlur() { + onBlur?.(); + setFocused(false); + } + + const addAsFilterText = + value && focused ? ( + + {i18n.translate('Polaris.IndexFilters.SearchField.action.addAsFilter')} + + + + + ) : undefined; + + return ( +
+ : undefined} + focused={focused} + label={ + placeholder ?? + i18n.translate('Polaris.IndexFilters.SearchField.defaultPlaceholder') + } + labelHidden + clearButton + loading={loading} + suffix={addAsFilterText} + /> +
+ ); +} diff --git a/polaris-react/src/components/IndexFilters/components/SearchField/index.ts b/polaris-react/src/components/IndexFilters/components/SearchField/index.ts new file mode 100644 index 00000000000..55415ea36b5 --- /dev/null +++ b/polaris-react/src/components/IndexFilters/components/SearchField/index.ts @@ -0,0 +1 @@ +export {SearchField} from './SearchField'; diff --git a/polaris-react/src/components/IndexFilters/components/SearchFilterButton/index.ts b/polaris-react/src/components/IndexFilters/components/SearchFilterButton/index.ts deleted file mode 100644 index 70da59a2ddc..00000000000 --- a/polaris-react/src/components/IndexFilters/components/SearchFilterButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {SearchFilterButton} from './SearchFilterButton'; diff --git a/polaris-react/src/components/IndexFilters/components/index.ts b/polaris-react/src/components/IndexFilters/components/index.ts index 956db0bf98b..9123cd62253 100644 --- a/polaris-react/src/components/IndexFilters/components/index.ts +++ b/polaris-react/src/components/IndexFilters/components/index.ts @@ -1,5 +1,6 @@ export {Container} from './Container'; -export {SearchFilterButton} from './SearchFilterButton'; +export {SearchField} from './SearchField'; +export {FilterButton} from './FilterButton'; export {SortButton} from './SortButton'; export {UpdateButtons} from './UpdateButtons'; export {EditColumnsButton} from './EditColumnsButton'; diff --git a/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx b/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx index ca857cb0f63..b9c2c007752 100644 --- a/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx +++ b/polaris-react/src/components/IndexFilters/tests/IndexFilters.test.tsx @@ -7,7 +7,7 @@ import {Filters} from '../../Filters'; import {IndexFilters, IndexFiltersMode} from '..'; import type {IndexFiltersProps} from '../IndexFilters'; import { - SearchFilterButton, + FilterButton, SortButton, UpdateButtons, EditColumnsButton, @@ -102,7 +102,7 @@ describe('IndexFilters', () => { , ); wrapper.act(() => { - wrapper.find(SearchFilterButton)!.trigger('onClick'); + wrapper.find(FilterButton)!.trigger('onClick'); }); expect(setMode).toHaveBeenCalledWith(IndexFiltersMode.Filtering); @@ -119,7 +119,7 @@ describe('IndexFilters', () => { />, ); wrapper.act(() => { - wrapper.find(SearchFilterButton)!.trigger('onClick'); + wrapper.find(FilterButton)!.trigger('onClick'); }); expect(onEditStart).toHaveBeenCalledWith(IndexFiltersMode.Filtering); @@ -155,15 +155,15 @@ describe('IndexFilters', () => { }); }); - it('renders the SearchFilterButton tooltipContent with keyboard shortcut by default', () => { + it('renders the FilterButton tooltipContent with keyboard shortcut by default', () => { const wrapper = mountWithApp(); - expect(wrapper).toContainReactComponent(SearchFilterButton, { + expect(wrapper).toContainReactComponent(FilterButton, { tooltipContent: 'Search and filter (F)', }); }); - it('passes the disclosureZIndexOverride to the SearchFilterButton when provided', () => { + it('passes the disclosureZIndexOverride to the FilterButton when provided', () => { const disclosureZIndexOverride = 517; const wrapper = mountWithApp( { />, ); - expect(wrapper).toContainReactComponent(SearchFilterButton, { + expect(wrapper).toContainReactComponent(FilterButton, { disclosureZIndexOverride, }); }); @@ -276,7 +276,7 @@ describe('IndexFilters', () => { expect(wrapper).not.toContainReactComponent(Filters); }); - it('does not render the SortButton or SearchFilterButton component', () => { + it('does not render the SortButton or FilterButton component', () => { const wrapper = mountWithApp( { ); expect(wrapper).not.toContainReactComponent(SortButton); - expect(wrapper).not.toContainReactComponent(SearchFilterButton); + expect(wrapper).not.toContainReactComponent(FilterButton); }); it('does not render the EditColumnsButton', () => { @@ -411,12 +411,12 @@ describe('IndexFilters', () => { }); describe('disableKeyboardShortcuts', () => { - it('renders the SearchFilterButton tooltipContent without the keyboard shortcut', () => { + it('renders the FilterButton tooltipContent without the keyboard shortcut', () => { const wrapper = mountWithApp( , ); - expect(wrapper).toContainReactComponent(SearchFilterButton, { + expect(wrapper).toContainReactComponent(FilterButton, { tooltipContent: 'Search and filter', }); }); diff --git a/polaris-react/src/types.ts b/polaris-react/src/types.ts index 26327bf76f3..f6cafbca92f 100644 --- a/polaris-react/src/types.ts +++ b/polaris-react/src/types.ts @@ -369,14 +369,20 @@ export type NonEmptyArray = [T, ...T[]]; export type ArrayElement = T extends (infer U)[] ? U : never; export interface AppliedFilterInterface { + /** Whether or not the filter can be removed */ + locked?: boolean; /** A unique key used to identify the filter */ key: string; - /** The name of the filter */ + /** The name of the filter. + * The rendered applied filter pill label will prefix the value with the label in standardized format. For example, label 'Product vender' and value ['Tootsie Roll Industries LLC' or 'The Hershey Company'] will be parsed into human readable label 'Product vendor: Tootsie Roll Industries LLC or The Hershey Company' + */ label: string; + /** The human readable filter input value */ + value?: any; /** Whether the filter is newly applied or updated and hasn't been saved */ unsavedChanges?: boolean; /** Callback when the remove button is pressed */ - onRemove(key: string): void; + onRemove?(key: string): void; } export interface FilterInterface { @@ -384,6 +390,8 @@ export interface FilterInterface { key: string; /** The name of the filter */ label: string; + /** The current filter input value */ + value?: any; /** The markup for the given filter */ filter: React.ReactNode; /** Whether or not the filter should have a shortcut popover displayed */