feat(dashboard): configurable product views (#13408)
* feat: add a reusable configurable data table * fix: cleanup * fix: cleanup * fix: cache invalidation * fix: test * fix: add configurable products * feat: add configurable product table * fix: build errors+table style * fix: sticky header column * add translations * fix: cleanup counterenderer * fix: formatting * fix: client still skips nulls * fix: test * fix: cleanup * fix: revert client bracket format * fix: better typing * fix: add placeholder data to product list
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Outlet, useLocation } from "react-router-dom"
|
||||
|
||||
import { ConfigurableDataTable } from "../../../../../components/table/configurable-data-table"
|
||||
import { useProductTableAdapter } from "./product-table-adapter"
|
||||
|
||||
export const ConfigurableProductListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
const location = useLocation()
|
||||
const adapter = useProductTableAdapter()
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigurableDataTable
|
||||
adapter={adapter}
|
||||
heading={t("products.domain")}
|
||||
actions={[
|
||||
{ label: t("actions.export"), to: `export${location.search}` },
|
||||
{ label: t("actions.import"), to: "import" },
|
||||
{ label: t("actions.create"), to: "create" }
|
||||
]}
|
||||
/>
|
||||
<Outlet />
|
||||
</>
|
||||
)
|
||||
}
|
||||
+8
@@ -18,12 +18,20 @@ import { useProductTableFilters } from "../../../../../hooks/table/filters/use-p
|
||||
import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
import { productsLoader } from "../../loader"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
import { ConfigurableProductListTable } from "./configurable-product-list-table"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const ProductListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
const location = useLocation()
|
||||
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
||||
|
||||
// If feature flag is enabled, use the new configurable table
|
||||
if (isViewConfigEnabled) {
|
||||
return <ConfigurableProductListTable />
|
||||
}
|
||||
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<ReturnType<typeof productsLoader>>
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useProducts } from "../../../../../hooks/api/products"
|
||||
import { productColumnAdapter } from "../../../../../lib/table/entity-adapters"
|
||||
import { createTableAdapter, TableAdapter } from "../../../../../lib/table/table-adapters"
|
||||
import { useProductTableFilters } from "./use-product-table-filters"
|
||||
|
||||
export function createProductTableAdapter(): TableAdapter<HttpTypes.AdminProduct> {
|
||||
return createTableAdapter<HttpTypes.AdminProduct>({
|
||||
entity: "products",
|
||||
queryPrefix: "p",
|
||||
pageSize: 20,
|
||||
columnAdapter: productColumnAdapter,
|
||||
useData: (fields, params) => {
|
||||
const { products, count, isError, error, isLoading } = useProducts(
|
||||
{
|
||||
fields,
|
||||
...params,
|
||||
is_giftcard: false, // Exclude gift cards from product list
|
||||
},
|
||||
{
|
||||
placeholderData: (previousData, previousQuery) => {
|
||||
// Only keep placeholder data if the fields haven't changed
|
||||
const prevFields = previousQuery?.[previousQuery.length - 1]?.query?.fields
|
||||
if (prevFields && prevFields !== fields) {
|
||||
// Fields changed, don't use placeholder data
|
||||
return undefined
|
||||
}
|
||||
// Fields are the same, keep previous data for smooth transitions
|
||||
return previousData
|
||||
},
|
||||
}
|
||||
)
|
||||
return { data: products, count, isLoading, isError, error }
|
||||
},
|
||||
getRowHref: (row) => `/products/${row.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get the product table adapter with filters
|
||||
*/
|
||||
export function useProductTableAdapter(): TableAdapter<HttpTypes.AdminProduct> {
|
||||
const filters = useProductTableFilters()
|
||||
const adapter = createProductTableAdapter()
|
||||
|
||||
// Add dynamic filters to the adapter
|
||||
return {
|
||||
...adapter,
|
||||
filters,
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { createDataTableFilterHelper } from "@medusajs/ui"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useDataTableDateFilters } from "../../../../../components/data-table/helpers/general/use-data-table-date-filters"
|
||||
import { useProductTypes } from "../../../../../hooks/api/product-types"
|
||||
import { useProductTags } from "../../../../../hooks/api"
|
||||
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||
|
||||
const filterHelper = createDataTableFilterHelper<HttpTypes.AdminProduct>()
|
||||
|
||||
/**
|
||||
* Hook to create filters in the format expected by @medusajs/ui DataTable
|
||||
*/
|
||||
export const useProductTableFilters = () => {
|
||||
const { t } = useTranslation()
|
||||
const dateFilters = useDataTableDateFilters()
|
||||
|
||||
const { product_types } = useProductTypes({
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
})
|
||||
|
||||
const { product_tags } = useProductTags({
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
})
|
||||
|
||||
const { sales_channels } = useSalesChannels({
|
||||
limit: 1000,
|
||||
fields: "id,name",
|
||||
})
|
||||
|
||||
return useMemo(() => {
|
||||
const filters = [...dateFilters]
|
||||
|
||||
if (product_types?.length) {
|
||||
filters.push(
|
||||
filterHelper.accessor("type_id", {
|
||||
label: t("fields.type"),
|
||||
type: "multiselect",
|
||||
options: product_types.map((t) => ({
|
||||
label: t.value,
|
||||
value: t.id,
|
||||
})),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (product_tags?.length) {
|
||||
filters.push(
|
||||
filterHelper.accessor("tag_id", {
|
||||
label: t("fields.tag"),
|
||||
type: "multiselect",
|
||||
options: product_tags.map((t) => ({
|
||||
label: t.value,
|
||||
value: t.id,
|
||||
})),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (sales_channels?.length) {
|
||||
filters.push(
|
||||
filterHelper.accessor("sales_channel_id", {
|
||||
label: t("fields.salesChannel"),
|
||||
type: "multiselect",
|
||||
options: sales_channels.map((s) => ({
|
||||
label: s.name,
|
||||
value: s.id,
|
||||
})),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Status filter
|
||||
filters.push(
|
||||
filterHelper.accessor("status", {
|
||||
label: t("fields.status"),
|
||||
type: "multiselect",
|
||||
options: [
|
||||
{
|
||||
label: t("products.productStatus.draft"),
|
||||
value: "draft",
|
||||
},
|
||||
{
|
||||
label: t("products.productStatus.proposed"),
|
||||
value: "proposed",
|
||||
},
|
||||
{
|
||||
label: t("products.productStatus.published"),
|
||||
value: "published",
|
||||
},
|
||||
{
|
||||
label: t("products.productStatus.rejected"),
|
||||
value: "rejected",
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
return filters
|
||||
}, [product_types, product_tags, sales_channels, dateFilters, t])
|
||||
}
|
||||
Reference in New Issue
Block a user