Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/app-elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@commercelayer/sdk": "8.0.0-beta.11",
"@date-fns/tz": "^1.5.0",
"@monaco-editor/react": "~4.7.0",
"@tanstack/react-table": "^9.0.0",
"@types/lodash-es": "^4.17.12",
"@types/react": "19.2.13",
"@types/react-datepicker": "^7.0.0",
Expand Down
8 changes: 8 additions & 0 deletions packages/app-elements/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,12 @@ export {
type UseResourceListConfig,
useResourceList,
} from "#ui/resources/useResourceList"
export {
type ResourceTableColumn,
type ResourceTableProps,
type ResourceTableSort,
type UseResourceTableConfig,
type UseResourceTableReturn,
useResourceTable,
} from "#ui/resources/useResourceTable"
export { useTrackingDetails } from "#ui/resources/useTrackingDetails"
14 changes: 14 additions & 0 deletions packages/app-elements/src/ui/atoms/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ export interface ContainerProps {
* Set min height as screen size. Default is `true`.
*/
minHeight?: boolean
/**
* Let the content span all the available width instead of being constrained
* to the standard readable column (632px from the `md` breakpoint up).
*
* Use it for data-dense pages such as tables. When the app runs inside the
* dashboard, the horizontal breathing room comes from the dashboard layout,
* which also needs to render the route without the legacy side column.
* @default false
*/
fullWidth?: boolean
/**
* CSS class name
*/
Expand All @@ -20,13 +30,17 @@ export const Container: React.FC<ContainerProps> = ({
children,
className,
minHeight = true,
fullWidth = false,
...rest
}) => {
return (
<div
className={cn(
"container mx-auto flex flex-col px-4 md:px-0",
{ "min-h-screen": minHeight },
// `md:max-w-none` opts out of the capped width set by the `container`
// utility (see styles/global.css)
{ "w-full md:max-w-none": fullWidth },
className,
)}
{...rest}
Expand Down
8 changes: 6 additions & 2 deletions packages/app-elements/src/ui/composite/HomePageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import type { JSX, ReactNode } from "react"
import { useTokenProvider } from "#providers/TokenProvider"
import type { PageHeadingProps } from "#ui/atoms/PageHeading"
import type { PageHeadingToolbarProps } from "#ui/atoms/PageHeading/PageHeadingToolbar"
import { PageLayout } from "./PageLayout"
import { PageLayout, type PageLayoutProps } from "./PageLayout"

export interface HomePageLayoutProps extends Pick<PageHeadingProps, "title"> {
export interface HomePageLayoutProps
extends Pick<PageHeadingProps, "title">,
Pick<PageLayoutProps, "fullWidth"> {
/**
* Page content
*/
Expand All @@ -23,6 +25,7 @@ export function HomePageLayout({
title,
children,
toolbar,
fullWidth,
}: HomePageLayoutProps): JSX.Element {
const {
settings: { mode, dashboardUrl, isInDashboard, onAppClose },
Expand All @@ -34,6 +37,7 @@ export function HomePageLayout({
mode={mode}
gap="only-top"
scrollToTop
fullWidth={fullWidth}
navigationButton={
isInDashboard && onAppClose == null
? undefined
Expand Down
12 changes: 10 additions & 2 deletions packages/app-elements/src/ui/composite/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type PageLayoutProps = Pick<
PageHeadingProps,
"title" | "description" | "navigationButton" | "toolbar" | "gap"
> &
Pick<ContainerProps, "minHeight"> & {
Pick<ContainerProps, "minHeight" | "fullWidth"> & {
/**
* Page content
*/
Expand Down Expand Up @@ -52,6 +52,7 @@ export const PageLayout = withSkeletonTemplate<PageLayoutProps>(
mode,
gap,
minHeight,
fullWidth,
scrollToTop,
overlay = false,
isLoading,
Expand Down Expand Up @@ -98,7 +99,14 @@ export const PageLayout = withSkeletonTemplate<PageLayoutProps>(
}

return (
<Container minHeight={minHeight} {...rest}>
<Container
minHeight={minHeight}
fullWidth={fullWidth}
// Inside the dashboard the horizontal breathing room comes from the
// dashboard layout; standalone there is nothing else to provide it.
className={fullWidth === true && !isInDashboard ? "md:px-8" : undefined}
{...rest}
>
<Spacer bottom="14">{component}</Spacer>
</Container>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
} from "./types"

export interface FilterSearchBarProps
extends Pick<SearchBarProps, "placeholder" | "debounceMs"> {
extends Pick<SearchBarProps, "placeholder" | "debounceMs" | "variant"> {
/**
* Array of instruction items to build the filters behaviors
*/
Expand Down Expand Up @@ -49,6 +49,7 @@ function FiltersSearchBar({
queryString,
predicateWhitelist,
debounceMs,
variant,
}: FilterSearchBarProps): JSX.Element {
const { adaptUrlQueryToFormValues, adaptFormValuesToUrlQuery } =
makeFilterAdapters({
Expand Down Expand Up @@ -97,6 +98,7 @@ function FiltersSearchBar({
onSearch={updateTextFilter}
autoFocus={safeInitialValue !== undefined && safeInitialValue.length > 0}
debounceMs={debounceMs}
variant={variant}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type JSX, useCallback, useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { useTokenProvider } from "#providers/TokenProvider"
import { Spacer } from "#ui/atoms/Spacer"
import type { SearchBarProps } from "#ui/composite/SearchBar"
import {
type UseResourceListConfig,
useResourceList,
Expand All @@ -11,6 +12,11 @@ import type {
ResourceListProps,
UseResourceListReturnWithPagination,
} from "#ui/resources/useResourceList/useResourceList"
import { useResourceTable } from "#ui/resources/useResourceTable"
import type {
ResourceTableProps,
UseResourceTableConfig,
} from "#ui/resources/useResourceTable/types"
import { makeFilterAdapters } from "./adapters"
import {
FiltersForm as FiltersFormComponent,
Expand Down Expand Up @@ -62,6 +68,11 @@ interface UseResourceFiltersHook {
* @default 'Search...'
*/
searchBarPlaceholder?: string
/**
* Visual variant of the search bar. Use `outline` to match the style used
* in the dashboard (metrics) pages.
*/
searchBarVariant?: SearchBarProps["variant"]
/**
* Milliseconds to wait before triggering the search bar callback
* @default 500
Expand Down Expand Up @@ -93,6 +104,28 @@ interface UseResourceFiltersHook {
hideTitle?: boolean
},
) => React.ReactNode
/**
* Filtered ResourceTable component based on current active filters.
* Table sibling of `FilteredList`: renders a column-model data table wired
* to the active search/filters and pagination.
*/
FilteredTable: <TResource extends ListableResourceType>(
props: Omit<UseResourceTableConfig<TResource>, "query" | "metricsQuery"> &
ResourceTableProps & {
query?: Omit<
NonNullable<UseResourceTableConfig<TResource>["query"]>,
"filters"
>
metricsQuery?: Omit<
NonNullable<UseResourceTableConfig<TResource>["metricsQuery"]>,
"filter"
> & {
/** Filters need to be configured within the `useResourceFilters` options. */
filter?: never
}
hideTitle?: boolean
},
) => React.ReactNode
/**
* SDK filters object to be used in the sdk query
*/
Expand Down Expand Up @@ -148,6 +181,11 @@ export function useResourceFilters({
[sdkFilters],
)

const FilteredTable = useMemo(
() => makeFilteredTable({ sdkFilters, adapters }),
[sdkFilters],
)

const SearchWithNav = useMemo(() => {
return makeSearchWithNav({
validInstructions,
Expand Down Expand Up @@ -187,6 +225,7 @@ export function useResourceFilters({
SearchWithNav,
FiltersForm,
FilteredList,
FilteredTable,
viewTitle,
}
}
Expand Down Expand Up @@ -266,6 +305,83 @@ const makeFilteredList: (options: {
)
}

// internal implementation of the ResourceTable component exposed from the useResourceTable hook
function ResourceTableComponent<TResource extends ListableResourceType>({
type,
columns,
query,
metricsQuery,
preProcess,
paginationType = "pagination",
paginationScrollTo,
onRowClick,
getRowHref,
sort,
onSortChange,
defaultSort,
...tableProps
}: UseResourceTableConfig<TResource> & ResourceTableProps): JSX.Element {
const { ResourceTable, Pagination } = useResourceTable<TResource>({
type,
columns,
query,
metricsQuery,
preProcess,
paginationType,
paginationScrollTo,
onRowClick,
getRowHref,
sort,
onSortChange,
defaultSort,
})

return (
<>
<ResourceTable {...tableProps} />
<Pagination />
</>
)
}

const makeFilteredTable: (options: {
sdkFilters: QueryFilter | undefined
adapters: ReturnType<typeof makeFilterAdapters>
}) => UseResourceFiltersHook["FilteredTable"] =
({ sdkFilters, adapters }) =>
({ type, query, metricsQuery, hideTitle, ...tableProps }) => {
const { t } = useTranslation()

if (sdkFilters == null) {
return null
}

return (
<ResourceTableComponent
{...tableProps}
type={type}
title={
hideTitle === true ? undefined : (tableProps.title ?? t("common.all"))
}
query={{
...query,
filters: sdkFilters,
}}
metricsQuery={
metricsQuery == null
? undefined
: {
...metricsQuery,
filter: adapters.adaptSdkToMetrics({
sdkFilters,
resourceType: type,
}),
}
}
/>
)
}

const makeSearchWithNav: (_options: {
validInstructions: FiltersInstructions
predicateWhitelist: string[]
Expand All @@ -276,6 +392,7 @@ const makeSearchWithNav: (_options: {
onUpdate,
searchBarPlaceholder,
searchBarDebounceMs,
searchBarVariant,
hideSearchBar,
hideFiltersNav,
// we need this value as prop to avoid re-rendering the component and losing the focus on searchbar
Expand All @@ -296,6 +413,7 @@ const makeSearchWithNav: (_options: {
queryString={queryStringProp}
placeholder={searchBarPlaceholder ?? t("common.search")}
debounceMs={searchBarDebounceMs}
variant={searchBarVariant}
instructions={validInstructions}
onUpdate={onUpdate}
predicateWhitelist={predicateWhitelist}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Order } from "@commercelayer/sdk"
import type { Customer, Order } from "@commercelayer/sdk"
import type { CurrencyCode } from "#helpers/currencies"
import { formatCentsToCurrency } from "#ui/forms/InputCurrency"

Expand Down Expand Up @@ -73,6 +73,11 @@ export interface MetricsResourceOrder {
first_name?: string
last_name?: string
}
customer?: {
id?: string
email?: string
total_orders_count?: number
}
}

export function adaptMetricsOrderToCore(
Expand Down Expand Up @@ -279,5 +284,20 @@ export function adaptMetricsOrderToCore(
zip_code: metricsOrder.billing_address.zip_code,
}
: undefined,

customer:
metricsOrder.customer != null
? // `Customer` requires a `status` that the metrics payload does not
// carry. It is left absent rather than invented, so anything reading it
// sees "unknown" instead of a plausible but wrong value.
({
id: metricsOrder.customer.id ?? "",
created_at: "",
updated_at: "",
type: "customers",
email: metricsOrder.customer.email ?? "",
total_orders_count: metricsOrder.customer.total_orders_count,
} as Customer)
: undefined,
}
}
Loading
Loading