fix(dashboard,admin-shared): Fixes to inventory page (#8941)

**What**
- Adds qty column to reservations table.
- Adds missing injections zones
- Fixes layout on long description field.

Resolves CC-121
This commit is contained in:
Kasper Fabricius Kristensen authored and GitHub committed 2024-09-02 17:38:56 +00:00
1 parent 29555ae518
commit c6ff9efdea
5 files changed
+122 -71

No files matched your search

@@ -189,6 +189,15 @@ const RETURN_REASON_INJECTION_ZONES = [
"return_reason.list.after", "return_reason.list.after",
] as const ] as const
const INVENTORY_ITEM_INJECTION_ZONES = [
"inventory_item.details.before",
"inventory_item.details.after",
"inventory_item.details.side.before",
"inventory_item.details.side.after",
"inventory_item.list.before",
"inventory_item.list.after",
] as const
/** /**
* All valid injection zones in the admin panel. An injection zone is a specific place * All valid injection zones in the admin panel. An injection zone is a specific place
* in the admin panel where a plugin can inject custom widgets. * in the admin panel where a plugin can inject custom widgets.
@@ -220,4 +229,5 @@ export const INJECTION_ZONES = [
...PRODUCT_TYPE_INJECTION_ZONES, ...PRODUCT_TYPE_INJECTION_ZONES,
...PRODUCT_TAG_INJECTION_ZONES, ...PRODUCT_TAG_INJECTION_ZONES,
...RETURN_REASON_INJECTION_ZONES, ...RETURN_REASON_INJECTION_ZONES,
...INVENTORY_ITEM_INJECTION_ZONES,
] as const ] as const
@@ -1,28 +1,48 @@
import { clx } from "@medusajs/ui"
import { ConditionalTooltip } from "../../../../common/conditional-tooltip"
import { PlaceholderCell } from "../placeholder-cell" import { PlaceholderCell } from "../placeholder-cell"
type CellProps = { type CellProps = {
text?: string | number text?: string | number
align?: "left" | "center" | "right"
maxWidth?: number
} }
type HeaderProps = { type HeaderProps = {
text: string text: string
align?: "left" | "center" | "right"
} }
export const TextCell = ({ text }: CellProps) => { export const TextCell = ({ text, align = "left", maxWidth = 220 }: CellProps) => {
if (!text) { if (!text) {
return <PlaceholderCell /> return <PlaceholderCell />
} }
const stringLength = text.toString().length
return ( return (
<div className="flex h-full w-full items-center gap-x-3 overflow-hidden"> <ConditionalTooltip content={text} showTooltip={stringLength > 20}>
<div className={clx("flex h-full w-full items-center gap-x-3 overflow-hidden", {
"justify-start text-start": align === "left",
"justify-center text-center": align === "center",
"justify-end text-end": align === "right",
})}
style={{
maxWidth: maxWidth,
}}>
<span className="truncate">{text}</span> <span className="truncate">{text}</span>
</div> </div>
</ConditionalTooltip>
) )
} }
export const TextHeader = ({ text }: HeaderProps) => { export const TextHeader = ({ text, align = "left" }: HeaderProps) => {
return ( return (
<div className="flex h-full w-full items-center"> <div className={clx("flex h-full w-full items-center", {
"justify-start text-start": align === "left",
"justify-center text-center": align === "center",
"justify-end text-end": align === "right",
})}>
<span className="truncate">{text}</span> <span className="truncate">{text}</span>
</div> </div>
) )
@@ -1,7 +1,7 @@
import { Container, Heading, IconButton } from "@medusajs/ui" import { TriangleRightMini } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { ArrowUpRightOnBox } from "@medusajs/icons" import { Link } from "react-router-dom"
import { useNavigate } from "react-router-dom"
import { ProductVariantDTO } from "@medusajs/types" import { ProductVariantDTO } from "@medusajs/types"
import { Thumbnail } from "../../../../../components/common/thumbnail" import { Thumbnail } from "../../../../../components/common/thumbnail"
@@ -14,7 +14,6 @@ export const InventoryItemVariantsSection = ({
variants, variants,
}: InventoryItemVariantsSectionProps) => { }: InventoryItemVariantsSectionProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate()
if (!variants?.length) { if (!variants?.length) {
return null return null
@@ -27,11 +26,13 @@ export const InventoryItemVariantsSection = ({
</div> </div>
<div className="txt-small flex flex-col gap-2 px-2 pb-2"> <div className="txt-small flex flex-col gap-2 px-2 pb-2">
{variants.map((variant) => ( {variants.map((variant) => {
<div const link = variant.product
key={variant.id} ? `/products/${variant.product.id}/variants/${variant.id}`
className="shadow-elevation-card-rest bg-ui-bg-component rounded-md px-4 py-2" : null
>
const Inner = (
<div className="shadow-elevation-card-rest bg-ui-bg-component rounded-md px-4 py-2 transition-colors">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="shadow-elevation-card-rest rounded-md"> <div className="shadow-elevation-card-rest rounded-md">
<Thumbnail src={variant.product?.thumbnail} /> <Thumbnail src={variant.product?.thumbnail} />
@@ -44,21 +45,27 @@ export const InventoryItemVariantsSection = ({
{variant.options.map((o) => o.value).join(" ⋅ ")} {variant.options.map((o) => o.value).join(" ⋅ ")}
</span> </span>
</div> </div>
<IconButton <div className="size-7 flex items-center justify-center">
size="2xsmall" <TriangleRightMini className="text-ui-fg-muted" />
variant="transparent" </div>
type="button" </div>
onClick={() => </div>
navigate(
`/products/${variant.product.id}/variants/${variant.id}`
) )
if (!link) {
return <div key={variant.id}>{Inner}</div>
} }
return (
<Link
to={link}
key={variant.id}
className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md [&:hover>div]:bg-ui-bg-component-hover"
> >
<ArrowUpRightOnBox className="text-ui-fg-muted" /> {Inner}
</IconButton> </Link>
</div> )
</div> })}
))}
</div> </div>
</Container> </Container>
) )
@@ -1,10 +1,11 @@
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
import { ReservationActions } from "./reservation-actions"
import { createColumnHelper } from "@tanstack/react-table" import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { CreatedAtCell } from "../../../../../components/table/table-cells/common/created-at-cell" import { CreatedAtCell } from "../../../../../components/table/table-cells/common/created-at-cell"
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
import { TextCell, TextHeader } from "../../../../../components/table/table-cells/common/text-cell"
import { ReservationActions } from "./reservation-actions"
/** /**
* Adds missing properties to the InventoryItemDTO type. * Adds missing properties to the InventoryItemDTO type.
@@ -22,17 +23,16 @@ export const useReservationTableColumn = ({ sku }: { sku: string }) => {
return useMemo( return useMemo(
() => [ () => [
columnHelper.display({ columnHelper.display({
header: t("fields.sku"), id: "sku",
header: () => <TextHeader text={t("fields.sku")} />,
cell: () => { cell: () => {
return ( return (
<div className="flex size-full items-center overflow-hidden"> <TextCell text={sku} />
<span className="truncate">{sku}</span>
</div>
) )
}, },
}), }),
columnHelper.accessor("line_item.order_id", { columnHelper.accessor("line_item.order_id", {
header: t("inventory.reservation.orderID"), header: () => <TextHeader text={t("inventory.reservation.orderID")} />,
cell: ({ getValue }) => { cell: ({ getValue }) => {
const orderId = getValue() const orderId = getValue()
@@ -41,14 +41,12 @@ export const useReservationTableColumn = ({ sku }: { sku: string }) => {
} }
return ( return (
<div className="flex size-full items-center overflow-hidden"> <TextCell text={orderId} />
<span className="truncate">{orderId}</span>
</div>
) )
}, },
}), }),
columnHelper.accessor("description", { columnHelper.accessor("description", {
header: t("fields.description"), header: () => <TextHeader text={t("fields.description")} />,
cell: ({ getValue }) => { cell: ({ getValue }) => {
const description = getValue() const description = getValue()
@@ -57,14 +55,12 @@ export const useReservationTableColumn = ({ sku }: { sku: string }) => {
} }
return ( return (
<div className="flex size-full items-center overflow-hidden"> <TextCell text={description} />
<span className="truncate">{description}</span>
</div>
) )
}, },
}), }),
columnHelper.accessor("location.name", { columnHelper.accessor("location.name", {
header: t("inventory.reservation.location"), header: () => <TextHeader text={t("inventory.reservation.location")} />,
cell: ({ getValue }) => { cell: ({ getValue }) => {
const location = getValue() const location = getValue()
@@ -73,16 +69,20 @@ export const useReservationTableColumn = ({ sku }: { sku: string }) => {
} }
return ( return (
<div className="flex size-full items-center overflow-hidden"> <TextCell text={location} />
<span className="truncate">{location}</span>
</div>
) )
}, },
}), }),
columnHelper.accessor("created_at", { columnHelper.accessor("created_at", {
header: t("fields.createdAt"), header: () => <TextHeader text={t("fields.createdAt")} />,
cell: ({ getValue }) => <CreatedAtCell date={getValue()} />, cell: ({ getValue }) => <CreatedAtCell date={getValue()} />,
}), }),
columnHelper.accessor("quantity", {
header: () => <TextHeader text={t("fields.quantity")} align="right" />,
cell: ({ getValue }) => {
return <TextCell text={getValue()} align="right" />
},
}),
columnHelper.display({ columnHelper.display({
id: "actions", id: "actions",
cell: ({ row }) => <ReservationActions reservation={row.original} />, cell: ({ row }) => <ReservationActions reservation={row.original} />,
@@ -1,14 +1,20 @@
import { Outlet, json, useLoaderData, useParams } from "react-router-dom" import { useLoaderData, useParams } from "react-router-dom"
import { TwoColumnPageSkeleton } from "../../../components/common/skeleton"
import { TwoColumnPage } from "../../../components/layout/pages"
import { useInventoryItem } from "../../../hooks/api/inventory"
import { InventoryItemAttributeSection } from "./components/inventory-item-attributes/attributes-section" import { InventoryItemAttributeSection } from "./components/inventory-item-attributes/attributes-section"
import { InventoryItemGeneralSection } from "./components/inventory-item-general-section" import { InventoryItemGeneralSection } from "./components/inventory-item-general-section"
import { InventoryItemLocationLevelsSection } from "./components/inventory-item-location-levels" import { InventoryItemLocationLevelsSection } from "./components/inventory-item-location-levels"
import { InventoryItemReservationsSection } from "./components/inventory-item-reservations" import { InventoryItemReservationsSection } from "./components/inventory-item-reservations"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { useInventoryItem } from "../../../hooks/api/inventory"
import { InventoryItemVariantsSection } from "./components/inventory-item-variants/variants-section" import { InventoryItemVariantsSection } from "./components/inventory-item-variants/variants-section"
import { inventoryItemLoader } from "./loader" import { inventoryItemLoader } from "./loader"
import after from "virtual:medusa/widgets/inventory_item/details/after"
import before from "virtual:medusa/widgets/inventory_item/details/before"
import sideAfter from "virtual:medusa/widgets/inventory_item/details/side/after"
import sideBefore from "virtual:medusa/widgets/inventory_item/details/side/before"
export const InventoryDetail = () => { export const InventoryDetail = () => {
const { id } = useParams() const { id } = useParams()
@@ -31,37 +37,45 @@ export const InventoryDetail = () => {
} }
) )
if (isLoading) { if (isLoading || !inventory_item) {
return <div>Loading...</div> return (
<TwoColumnPageSkeleton
showJSON
showMetadata
mainSections={3}
sidebarSections={2}
/>
)
} }
if (isError || !inventory_item) {
if (error) { if (isError) {
throw error throw error
} }
throw json("An unknown error occurred", 500)
}
return ( return (
<div className="flex flex-col gap-y-2"> <TwoColumnPage
<div className="flex flex-col gap-x-4 lg:flex-row lg:items-start"> widgets={{
<div className="flex w-full flex-col gap-y-3"> after,
before,
sideAfter,
sideBefore,
}}
data={inventory_item}
showJSON
showMetadata
hasOutlet
>
<TwoColumnPage.Main>
<InventoryItemGeneralSection inventoryItem={inventory_item} /> <InventoryItemGeneralSection inventoryItem={inventory_item} />
<InventoryItemLocationLevelsSection inventoryItem={inventory_item} /> <InventoryItemLocationLevelsSection inventoryItem={inventory_item} />
<InventoryItemReservationsSection inventoryItem={inventory_item} /> <InventoryItemReservationsSection inventoryItem={inventory_item} />
<div className="hidden lg:block"> </TwoColumnPage.Main>
<JsonViewSection data={inventory_item} /> <TwoColumnPage.Sidebar>
</div> <InventoryItemVariantsSection
<Outlet /> variants={(inventory_item as any).variants}
</div> />
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 lg:mt-0 lg:max-w-[400px]"> <InventoryItemAttributeSection inventoryItem={inventory_item as any} />
<InventoryItemVariantsSection variants={inventory_item.variants} /> </TwoColumnPage.Sidebar>
<InventoryItemAttributeSection inventoryItem={inventory_item} /> </TwoColumnPage>
<div className="lg:hidden">
<JsonViewSection data={inventory_item} />
</div>
</div>
</div>
</div>
) )
} }