Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/app-elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"dependencies": {
"@commercelayer/js-auth": "^7.4.2",
"@commercelayer/provisioning-sdk": "2.10.2",
"@commercelayer/sdk": "8.0.0-beta.11",
"@commercelayer/sdk": "8.0.0-beta.12",
"@date-fns/tz": "^1.5.0",
"@monaco-editor/react": "~4.7.0",
"@tanstack/react-table": "^9.0.0",
Expand Down
2 changes: 0 additions & 2 deletions packages/app-elements/src/helpers/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ const singularLowercase: Record<ListableResourceType, string> = {
discount_engines: "discount engine",
easypost_pickups: "easypost pickup",
event_callbacks: "event callback",
event_stores: "event store",
events: "event",
exports: "export",
external_gateways: "external gateway",
Expand Down Expand Up @@ -175,7 +174,6 @@ const pluralLowercase: Record<ListableResourceType, string> = {
discount_engines: "discount engines",
easypost_pickups: "easypost pickups",
event_callbacks: "event callbacks",
event_stores: "event stores",
events: "events",
exports: "exports",
external_gateways: "external gateways",
Expand Down
4 changes: 4 additions & 0 deletions packages/app-elements/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,10 @@ export {
ResourceShipmentParcels,
type ResourceShipmentParcelsProps,
} from "#ui/resources/ResourceShipmentParcels"
export {
ResourceStatusBadge,
type ResourceStatusBadgeProps,
} from "#ui/resources/ResourceStatusBadge"
export {
isTaggableResource,
ResourceTags,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const Dropdown = withSkeletonTemplate<DropdownProps>(
aria-haspopup
aria-expanded={isExpanded}
className={cn("m-0 p-0 align-top", {
"text-black!": typeof dropdownLabel !== "string",
"text-gray-500!": typeof dropdownLabel !== "string",
"no-underline! hover:underline!": typeof dropdownLabel === "string",
})}
onClick={() => {
Expand Down
36 changes: 30 additions & 6 deletions packages/app-elements/src/ui/composite/ListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import cn from "classnames"
import type { FC, JSX } from "react"
import { FlexRow, type FlexRowProps } from "#ui/internals/FlexRow"
import {
useIsInBoxedList,
useIsLastRowInBoxedList,
} from "#ui/internals/listContext"
import { removeUnwantedProps } from "#utils/htmltags"

type ListItemVariant = "list" | "boxed"
Expand Down Expand Up @@ -32,8 +36,10 @@ export type ListItemProps = React.HTMLAttributes<HTMLElement> &
*/
paddingSize?: "6" | "4" | "2"
/**
* Border style to render
* @default 'solid'
* Border style to render.
*
* @default 'solid', or 'dashed' in a boxed list — where the rows sit on the
* card's own background and a solid rule would read as heavier than the group
*/
borderStyle?: "solid" | "dashed" | "none"
/**
Expand All @@ -56,11 +62,16 @@ export const ListItem: FC<ListItemProps> = ({
paddingSize = "4",
alignItems = "center",
alignIcon = "top",
borderStyle = "solid",
borderStyle: borderStyleProp,
variant = "list",
disabled = false,
...rest
}) => {
// A boxed list is nested in a parent resource's page: its rows show through to
// the card's gray, so they separate with a dashed rule rather than a solid one.
const isInBoxedList = useIsInBoxedList()
const closesBoxedList = useIsLastRowInBoxedList()
const borderStyle = borderStyleProp ?? (isInBoxedList ? "dashed" : "solid")
const wantedProps =
"overflow" in rest ? removeUnwantedProps(rest, ["overflow"]) : rest
const JsxTag =
Expand Down Expand Up @@ -107,8 +118,12 @@ export const ListItem: FC<ListItemProps> = ({
relative: borderStyle === "dashed",
"border-b": borderStyle === "solid",
"rounded border": variant === "boxed",
"hover:bg-gray-50 focus-visible:ring-primary focus-visible:ring-2 focus-visible:ring-inset focus-visible:outline-hidden":
"focus-visible:ring-primary focus-visible:ring-2 focus-visible:ring-inset focus-visible:outline-hidden":
isClickable,
// gray-50 is the boxed list's own background, so a row there needs the
// next step up to react to the pointer at all
"hover:bg-gray-50": isClickable && !isInBoxedList,
"hover:bg-gray-100": isClickable && isInBoxedList,
"bg-white": !disabled && variant === "boxed",
"bg-gray-100": disabled,
"border-gray-200": variant === "boxed" || disabled,
Expand Down Expand Up @@ -138,8 +153,17 @@ export const ListItem: FC<ListItemProps> = ({
)}
<FlexRow alignItems={alignItems}>{children}</FlexRow>
</div>
{borderStyle === "dashed" && (
<div className="absolute bottom-0 left-0 w-full h-px bg-[linear-gradient(to_right,transparent_50%,rgba(230,231,231,1)_50%)] bg-size-[10px_100%]" />
{borderStyle === "dashed" && !closesBoxedList && (
<div
className={cn(
"absolute bottom-0 left-0 w-full h-px bg-size-[10px_100%]",
// the default dash is picked for a white row; on the gray of a boxed
// list it would be all but invisible
isInBoxedList
? "bg-[linear-gradient(to_right,transparent_50%,var(--color-gray-200)_50%)]"
: "bg-[linear-gradient(to_right,transparent_50%,rgba(230,231,231,1)_50%)]",
)}
/>
)}
</JsxTag>
)
Expand Down
38 changes: 38 additions & 0 deletions packages/app-elements/src/ui/internals/listContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createContext, useContext } from "react"

/**
* Its own module, as `overlayContext` is, so that `ListItem` does not have to
* import the list that renders it.
*/
export const ListContext = createContext<{
boxed: boolean
/**
* `true` for the row that closes the list. Only the list knows this — its rows
* are a consumer's `ItemTemplate` — and a row cannot read it off the DOM: its
* separator is always its own last child, and the list's own last child may be
* a footer rather than a row.
*/
isLastRow: boolean
}>({
boxed: false,
isLastRow: false,
})

/**
* `true` when the calling row sits in a boxed list — a list nested in a parent
* resource's page rather than being the page itself.
*
* Rows read it instead of taking a prop, because the list cannot reach them: they
* come from a consumer-supplied `ItemTemplate`, so every app would otherwise have
* to forward a flag and a forgotten one would render the wrong separators with no
* error to show for it.
*/
export function useIsInBoxedList(): boolean {
return useContext(ListContext).boxed
}

/** `true` when the calling row is the last one of a boxed list. */
export function useIsLastRowInBoxedList(): boolean {
const { boxed, isLastRow } = useContext(ListContext)
return boxed && isLastRow
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const order = {
placed_at: "2023-06-09T11:00:00.000Z",
created_at: "",
formatted_total_amount: "$650.00",
formatted_total_amount_with_taxes: "$650.00",
fulfillment_status: "unfulfilled",
number: "123456",
payment_status: "authorized",
Expand Down Expand Up @@ -105,6 +106,7 @@ export const presetResourceListItem = {
payment_status: "paid",
fulfillment_status: "fulfilled",
formatted_total_amount: "$272.00",
formatted_total_amount_with_taxes: "$272.00",
market,
billing_address: {
first_name: "Ringo",
Expand All @@ -131,6 +133,7 @@ export const presetResourceListItem = {
payment_status: "unpaid",
fulfillment_status: "unfulfilled",
formatted_total_amount: "$272.00",
formatted_total_amount_with_taxes: "$272.00",
market,
},
orderPartiallyAuthorized: {
Expand All @@ -144,6 +147,7 @@ export const presetResourceListItem = {
payment_status: "partially_authorized",
fulfillment_status: "unfulfilled",
formatted_total_amount: "$272.00",
formatted_total_amount_with_taxes: "$272.00",
market,
billing_address: {
first_name: "Ringo",
Expand Down Expand Up @@ -171,6 +175,7 @@ export const presetResourceListItem = {
payment_status: "authorized",
fulfillment_status: "unfulfilled",
formatted_total_amount: "$272.00",
formatted_total_amount_with_taxes: "$272.00",
market,
billing_address: {
first_name: "Ringo",
Expand All @@ -197,6 +202,7 @@ export const presetResourceListItem = {
payment_status: "authorized",
fulfillment_status: "unfulfilled",
formatted_total_amount: "$272.00",
formatted_total_amount_with_taxes: "$272.00",
market,
billing_address: {
first_name: "Michele",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { type JSX, useMemo } from "react"
import { type JSX, type ReactNode, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useCoreApi } from "#providers/CoreSdkProvider"
import { useTokenProvider } from "#providers/TokenProvider"
import { withSkeletonTemplate } from "#ui/atoms/SkeletonTemplate"
import { StatusIcon } from "#ui/atoms/StatusIcon"
Expand All @@ -14,6 +13,7 @@ import {
skuListItemToProps,
stockTransferToProps,
} from "#ui/resources/ResourceListItem/transformers"
import { ResourceStatusBadge } from "#ui/resources/ResourceStatusBadge"
import { promotionToProps } from "./transformers/promotions"
import type {
ResourceListItemComponentProps,
Expand Down Expand Up @@ -41,6 +41,12 @@ export interface ResourceListItemProps {
* Optional override for the right slot. When provided, it replaces any computed right content.
*/
rightContentOverride?: JSX.Element | null
/**
* Rendered at the far right of the row, after the right content — a dropdown
* with what can be done to the resource, typically. A row with actions is
* usually not clickable itself: the menu is where the links live.
*/
actions?: ReactNode
}

type ResourceListItemConfig = Omit<ResourceListItemProps, "resource"> &
Expand All @@ -59,13 +65,15 @@ const ResourceListItemComponent = withSkeletonTemplate<ResourceListItemConfig>(
alignItems,
showRightContent = false,
invertNameDescription = false,
status,
actions,
}) => {
const isClickable = href != null || onClick != null

return (
<ListItem
icon={icon}
alignItems={alignItems ?? (showRightContent ? "top" : "center")}
alignItems={alignItems ?? (showRightContent ? "center" : "top")}
data-testid="ResourceListItem"
href={href}
onClick={onClick}
Expand All @@ -75,17 +83,20 @@ const ResourceListItemComponent = withSkeletonTemplate<ResourceListItemConfig>(
<div
className={`flex ${invertNameDescription ? "flex-col-reverse" : "flex-col"}`}
>
<Text
tag="div"
weight="semibold"
data-testid="ResourceListItem-number"
>
{name}
</Text>
<div className="flex items-center gap-2 leading-6">
<Text
tag="div"
weight="semibold"
data-testid="ResourceListItem-number"
>
{name}
</Text>
{status != null && <ResourceStatusBadge status={status} />}
</div>
<Text
tag="div"
weight="medium"
size="small"
size="x-small"
variant="info"
data-testid="ResourceListItem-content"
>
Expand All @@ -94,12 +105,15 @@ const ResourceListItemComponent = withSkeletonTemplate<ResourceListItemConfig>(
</div>
{bottomContent && <div className="mt-2">{bottomContent}</div>}
</div>
<div>
{rightContentOverride != null
? rightContentOverride
: showRightContent
? rightContent
: isClickable && <StatusIcon name="caretRight" />}
<div className="flex items-center gap-2">
<div>
{rightContentOverride != null
? rightContentOverride
: showRightContent
? rightContent
: isClickable && <StatusIcon name="caretRight" />}
</div>
{actions}
</div>
</ListItem>
)
Expand All @@ -114,41 +128,12 @@ export const ResourceListItem = withSkeletonTemplate<ResourceListItemProps>(
const { user } = useTokenProvider()
const { t } = useTranslation()

const { data: markets, isLoading: isLoadingMarkets } = useCoreApi(
"markets",
"list",
resource.type === "orders"
? [
{
fields: ["id"],
filters: {
disabled_at_null: true,
},
pageSize: 1,
},
]
: null,
{
revalidateIfStale: false,
},
)

const listItemProps = useMemo(() => {
switch (resource.type) {
case "customers":
return customerToProps({ resource, user, t })
case "orders":
return orderToProps({
resource: {
...resource,
market:
(markets?.meta.recordCount ?? 0) > 1
? resource.market
: undefined,
},
user,
t,
})
return orderToProps({ resource, user, t })
case "returns":
return returnToProps({ resource, user, t })
case "stock_transfers":
Expand All @@ -171,7 +156,7 @@ export const ResourceListItem = withSkeletonTemplate<ResourceListItemProps>(
return (
<ResourceListItemComponent
{...listItemProps}
isLoading={isLoadingMarkets || isLoading}
isLoading={isLoading}
href={href}
onClick={onClick}
{...rest}
Expand Down
Loading
Loading