fix(dashboard) editor columns (#7534)

* fix: reorg variant editor columns, auto select default sales channel when creating products

* fix: remove return

* fix: add placeholders on options create inputs, add placeholder for chip input

* fix: gaps on details pages

* fix: address feedback
This commit is contained in:
Frane Polić
2024-06-07 10:59:19 +02:00
committed by GitHub
parent 1f1b996f63
commit 987141ab2d
18 changed files with 92 additions and 185 deletions
@@ -97,7 +97,7 @@ export function getFieldsInRange(
} }
export function convertArrayToPrimitive< export function convertArrayToPrimitive<
T extends "boolean" | "number" | "string", T extends "boolean" | "number" | "string"
>(values: string[], type: T) { >(values: string[], type: T) {
const convertedValues: any[] = [] const convertedValues: any[] = []
@@ -172,7 +172,7 @@ type DataGridHelperColumnsProps<TData> = {
* *
* @default false * @default false
*/ */
disableHidding?: boolean disableHiding?: boolean
} }
export function createDataGridHelper<TData>() { export function createDataGridHelper<TData>() {
@@ -186,13 +186,13 @@ export function createDataGridHelper<TData>() {
cell, cell,
type = "string", type = "string",
asString, asString,
disableHidding = false, disableHiding = false,
}: DataGridHelperColumnsProps<TData>) => }: DataGridHelperColumnsProps<TData>) =>
columnHelper.display({ columnHelper.display({
id, id,
header, header,
cell, cell,
enableHiding: !disableHidding, enableHiding: !disableHiding,
meta: { meta: {
type, type,
asString, asString,
@@ -184,6 +184,12 @@
"header": "Variants", "header": "Variants",
"subHeadingTitle": "Yes, this is a product with variants", "subHeadingTitle": "Yes, this is a product with variants",
"subHeadingDescription": "When unchecked we will create a default variant for you", "subHeadingDescription": "When unchecked we will create a default variant for you",
"optionTitle": {
"placeholder": "Size"
},
"optionValues": {
"placeholder": "Small, Medium, Large"
},
"productVariants": { "productVariants": {
"label": "Product variants", "label": "Product variants",
"hint": "This ranking will affect how the variants are ranked in your frontend.", "hint": "This ranking will affect how the variants are ranked in your frontend.",
@@ -43,7 +43,7 @@ export const CampaignDetail = () => {
) )
})} })}
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start"> <div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2"> <div className="flex w-full flex-col gap-y-3">
<CampaignGeneralSection campaign={campaign} /> <CampaignGeneralSection campaign={campaign} />
<CampaignPromotionSection campaign={campaign} /> <CampaignPromotionSection campaign={campaign} />
{after.widgets.map((w, i) => { {after.widgets.map((w, i) => {
@@ -44,7 +44,7 @@ export const CategoryDetail = () => {
) )
})} })}
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start"> <div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2"> <div className="flex w-full flex-col gap-y-3">
<CategoryGeneralSection category={product_category} /> <CategoryGeneralSection category={product_category} />
<CategoryProductSection category={product_category} /> <CategoryProductSection category={product_category} />
{after.widgets.map((w, i) => { {after.widgets.map((w, i) => {
@@ -45,7 +45,7 @@ export const InventoryDetail = () => {
return ( return (
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-2">
<div className="flex flex-col gap-x-4 lg:flex-row lg:items-start"> <div className="flex flex-col gap-x-4 lg:flex-row lg:items-start">
<div className="flex w-full flex-col gap-y-2"> <div className="flex w-full flex-col gap-y-3">
<InventoryItemGeneralSection inventoryItem={inventory_item} /> <InventoryItemGeneralSection inventoryItem={inventory_item} />
<InventoryItemLocationLevelsSection inventoryItem={inventory_item} /> <InventoryItemLocationLevelsSection inventoryItem={inventory_item} />
<InventoryItemReservationsSection inventoryItem={inventory_item} /> <InventoryItemReservationsSection inventoryItem={inventory_item} />
@@ -52,7 +52,7 @@ export const LocationDetails = () => {
) )
})} })}
<div className="flex flex-col gap-x-4 lg:flex-row xl:items-start"> <div className="flex flex-col gap-x-4 lg:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2"> <div className="flex w-full flex-col gap-y-3">
<LocationGeneralSection location={location} /> <LocationGeneralSection location={location} />
{after.widgets.map((w, i) => { {after.widgets.map((w, i) => {
return ( return (
@@ -36,7 +36,7 @@ export const OrderFulfillmentSection = ({
const fulfillments = order.fulfillments || [] const fulfillments = order.fulfillments || []
return ( return (
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-3">
<UnfulfilledItemBreakdown order={order} /> <UnfulfilledItemBreakdown order={order} />
{fulfillments.map((f, index) => ( {fulfillments.map((f, index) => (
<Fulfillment key={f.id} index={index} fulfillment={f} order={order} /> <Fulfillment key={f.id} index={index} fulfillment={f} order={order} />
@@ -39,7 +39,7 @@ export const OrderDetail = () => {
} }
return ( return (
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-3">
{before.widgets.map((w, i) => { {before.widgets.map((w, i) => {
return ( return (
<div key={i}> <div key={i}>
@@ -48,7 +48,7 @@ export const OrderDetail = () => {
) )
})} })}
<div className="flex flex-col gap-x-4 lg:flex-row xl:items-start"> <div className="flex flex-col gap-x-4 lg:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2"> <div className="flex w-full flex-col gap-y-3">
<OrderGeneralSection order={order} /> <OrderGeneralSection order={order} />
<OrderSummarySection order={order} /> <OrderSummarySection order={order} />
{/* <OrderPaymentSection order={order} />*/} {/* <OrderPaymentSection order={order} />*/}
@@ -64,7 +64,7 @@ export const OrderDetail = () => {
<JsonViewSection data={order} /> <JsonViewSection data={order} />
</div> </div>
</div> </div>
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[400px]"> <div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[400px]">
{sideBefore.widgets.map((w, i) => { {sideBefore.widgets.map((w, i) => {
return ( return (
<div key={i}> <div key={i}>
@@ -5,7 +5,7 @@ import before from "virtual:medusa/widgets/order/list/before"
export const OrderList = () => { export const OrderList = () => {
return ( return (
<div className="flex w-full flex-col gap-y-2"> <div className="flex w-full flex-col gap-y-3">
{before.widgets.map((w, i) => { {before.widgets.map((w, i) => {
return ( return (
<div key={i}> <div key={i}>
@@ -34,7 +34,7 @@ export const PricingDetail = () => {
) )
})} })}
<div className="flex flex-col gap-x-4 lg:flex-row xl:items-start"> <div className="flex flex-col gap-x-4 lg:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2"> <div className="flex w-full flex-col gap-y-3">
<PricingGeneralSection priceList={price_list} /> <PricingGeneralSection priceList={price_list} />
<PricingProductSection priceList={price_list} /> <PricingProductSection priceList={price_list} />
{after.widgets.map((w, i) => { {after.widgets.map((w, i) => {
@@ -3,6 +3,7 @@ import { Button, ProgressStatus, ProgressTabs, toast } from "@medusajs/ui"
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { useForm, useWatch } from "react-hook-form" import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { SalesChannelDTO } from "@medusajs/types"
import { import {
RouteFocusModal, RouteFocusModal,
useRouteModal, useRouteModal,
@@ -35,7 +36,11 @@ const SAVE_DRAFT_BUTTON = "save-draft-button"
let LAST_VISITED_TAB: Tab | null = null let LAST_VISITED_TAB: Tab | null = null
export const ProductCreateForm = () => { type ProductCreateFormProps = { defaultChannel?: SalesChannelDTO }
export const ProductCreateForm = ({
defaultChannel,
}: ProductCreateFormProps) => {
const [tab, setTab] = useState<Tab>(Tab.DETAILS) const [tab, setTab] = useState<Tab>(Tab.DETAILS)
const [tabState, setTabState] = useState<TabState>({ const [tabState, setTabState] = useState<TabState>({
[Tab.DETAILS]: "in-progress", [Tab.DETAILS]: "in-progress",
@@ -48,7 +53,12 @@ export const ProductCreateForm = () => {
const { handleSuccess } = useRouteModal() const { handleSuccess } = useRouteModal()
const form = useForm<ProductCreateSchemaType>({ const form = useForm<ProductCreateSchemaType>({
defaultValues: PRODUCT_CREATE_FORM_DEFAULTS, defaultValues: {
...PRODUCT_CREATE_FORM_DEFAULTS,
sales_channels: defaultChannel
? [{ id: defaultChannel.id, name: defaultChannel.name }]
: [],
},
resolver: zodResolver(ProductCreateSchema), resolver: zodResolver(ProductCreateSchema),
}) })
@@ -1,4 +1,4 @@
import { ProductVariantDTO } from "@medusajs/types" import { ProductVariantDTO, RegionDTO } from "@medusajs/types"
import { useMemo } from "react" import { useMemo } from "react"
import { UseFormReturn, useWatch } from "react-hook-form" import { UseFormReturn, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
@@ -11,6 +11,7 @@ import { createDataGridHelper } from "../../../../../components/data-grid/utils"
import { DataGridCurrencyCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-currency-cell" import { DataGridCurrencyCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-currency-cell"
import { DataGridBooleanCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-boolean-cell" import { DataGridBooleanCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-boolean-cell"
import { DataGridCountrySelectCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-country-select-cell" import { DataGridCountrySelectCell } from "../../../../../components/data-grid/data-grid-cells/data-grid-country-select-cell"
import { useRegions } from "../../../../../hooks/api/regions"
type ProductCreateVariantsFormProps = { type ProductCreateVariantsFormProps = {
form: UseFormReturn<ProductCreateSchemaType> form: UseFormReturn<ProductCreateSchemaType>
@@ -19,9 +20,10 @@ type ProductCreateVariantsFormProps = {
export const ProductCreateVariantsForm = ({ export const ProductCreateVariantsForm = ({
form, form,
}: ProductCreateVariantsFormProps) => { }: ProductCreateVariantsFormProps) => {
const { regions } = useRegions({ limit: 9999 })
const { store, isPending, isError, error } = useStore({ const { store, isPending, isError, error } = useStore({
fields: "supported_currency_codes", fields: "supported_currency_codes",
limit: 9999,
}) })
const variants = useWatch({ const variants = useWatch({
@@ -39,6 +41,7 @@ export const ProductCreateVariantsForm = ({
const columns = useColumns({ const columns = useColumns({
options, options,
currencies: store?.supported_currency_codes, currencies: store?.supported_currency_codes,
regions,
}) })
const variantData = useMemo( const variantData = useMemo(
@@ -66,9 +69,11 @@ const columnHelper = createDataGridHelper<ProductVariantDTO>()
const useColumns = ({ const useColumns = ({
options, options,
currencies = [], currencies = [],
regions = [],
}: { }: {
options: any // CreateProductOptionSchemaType[] options: any // CreateProductOptionSchemaType[]
currencies?: string[] currencies?: string[]
regions: RegionDTO[]
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -90,7 +95,7 @@ const useColumns = ({
</DataGridReadOnlyCell> </DataGridReadOnlyCell>
) )
}, },
disableHidding: true, disableHiding: true,
}), }),
columnHelper.column({ columnHelper.column({
id: "title", id: "title",
@@ -118,45 +123,6 @@ const useColumns = ({
) )
}, },
}), }),
columnHelper.column({
id: "ean",
name: t("fields.ean"),
header: t("fields.ean"),
cell: (context) => {
return (
<DataGridTextCell
context={context}
field={`variants.${context.row.index}.ean`}
/>
)
},
}),
columnHelper.column({
id: "upc",
name: t("fields.upc"),
header: t("fields.upc"),
cell: (context) => {
return (
<DataGridTextCell
context={context}
field={`variants.${context.row.index}.upc`}
/>
)
},
}),
columnHelper.column({
id: "barcode",
name: t("fields.barcode"),
header: t("fields.barcode"),
cell: (context) => {
return (
<DataGridTextCell
context={context}
field={`variants.${context.row.index}.barcode`}
/>
)
},
}),
columnHelper.column({ columnHelper.column({
id: "manage_inventory", id: "manage_inventory",
@@ -189,7 +155,7 @@ const useColumns = ({
columnHelper.column({ columnHelper.column({
id: "inventory_kit", id: "inventory_kit",
name: t("fields.allowBackorder"), name: t("fields.inventoryKit"),
header: t("fields.inventoryKit"), header: t("fields.inventoryKit"),
cell: (context) => { cell: (context) => {
return ( return (
@@ -220,111 +186,23 @@ const useColumns = ({
}) })
}), }),
columnHelper.column({ ...regions.map((region) => {
id: "mid_code", return columnHelper.column({
name: t("fields.midCode"), id: `price_${region.id}`,
header: t("fields.midCode"), name: `Price ${region.name}`,
cell: (context) => { header: `Price ${region.name}`,
return ( cell: (context) => {
<DataGridTextCell return (
context={context} <DataGridCurrencyCell
field={`variants.${context.row.index}.mid_code`} code={region.currency_code}
/> context={context}
) field={`variants.${context.row.index}.prices.${region.id}`}
}, />
}), )
columnHelper.column({ },
id: "hs_code", })
name: t("fields.hsCode"),
header: t("fields.hsCode"),
cell: (context) => {
return (
<DataGridTextCell
context={context}
field={`variants.${context.row.index}.hs_code`}
/>
)
},
}),
columnHelper.column({
id: "width",
name: t("fields.width"),
header: t("fields.width"),
cell: (context) => {
return (
<DataGridTextCell
context={context}
field={`variants.${context.row.index}.width`}
/>
)
},
}),
columnHelper.column({
id: "length",
name: t("fields.length"),
header: t("fields.length"),
cell: (context) => {
return (
<DataGridTextCell
context={context}
field={`variants.${context.row.index}.length`}
/>
)
},
}),
columnHelper.column({
id: "height",
name: t("fields.height"),
header: t("fields.height"),
cell: (context) => {
return (
<DataGridTextCell
context={context}
field={`variants.${context.row.index}.height`}
/>
)
},
}),
columnHelper.column({
id: "weight",
name: t("fields.weight"),
header: t("fields.weight"),
cell: (context) => {
return (
<DataGridTextCell
context={context}
field={`variants.${context.row.index}.weight`}
/>
)
},
}),
columnHelper.column({
id: "material",
name: t("fields.material"),
header: t("fields.material"),
cell: (context) => {
return (
<DataGridTextCell
context={context}
field={`variants.${context.row.index}.material`}
/>
)
},
}),
columnHelper.column({
id: "origin_country",
name: t("fields.countryOfOrigin"),
header: t("fields.countryOfOrigin"),
cell: (context) => {
return (
<DataGridCountrySelectCell
context={context}
field={`variants.${context.row.index}.origin_country`}
/>
)
},
}), }),
], ],
[currencies, options, t] [currencies, regions, options, t]
) )
} }
@@ -1,10 +1,25 @@
import { RouteFocusModal } from "../../../components/route-modal" import { RouteFocusModal } from "../../../components/route-modal"
import { ProductCreateForm } from "./components/product-create-form/product-create-form" import { ProductCreateForm } from "./components/product-create-form/product-create-form"
import { useStore } from "../../../hooks/api/store"
import { useSalesChannel } from "../../../hooks/api/sales-channels"
export const ProductCreate = () => { export const ProductCreate = () => {
const { store } = useStore({
fields: "default_sales_channel",
})
const { sales_channel, isPending } = useSalesChannel(
store?.default_sales_channel_id,
{
enabled: !!store,
}
)
const canDisplayForm = store && !isPending
return ( return (
<RouteFocusModal> <RouteFocusModal>
<ProductCreateForm /> {canDisplayForm && <ProductCreateForm defaultChannel={sales_channel} />}
</RouteFocusModal> </RouteFocusModal>
) )
} }
@@ -56,10 +56,19 @@ export const normalizeVariants = (
manage_inventory: variant.manage_inventory || undefined, manage_inventory: variant.manage_inventory || undefined,
allow_backorder: variant.allow_backorder || undefined, allow_backorder: variant.allow_backorder || undefined,
// TODO: inventory - should be added to the workflow // TODO: inventory - should be added to the workflow
prices: Object.entries(variant.prices || {}).map(([key, value]: any) => ({ prices: Object.entries(variant.prices || {})
currency_code: key, .map(([key, value]: any) => {
amount: getDbAmount(castNumber(value), key), if (key.startsWith("reg_")) {
})), // TODO: route needs to accept region prices as well
return undefined
} else {
return {
currency_code: key,
amount: getDbAmount(castNumber(value), key),
}
}
})
.filter((v) => !!v),
})) }))
} }
@@ -70,19 +79,8 @@ export const decorateVariantsWithDefaultValues = (
...variant, ...variant,
title: variant.title || "", title: variant.title || "",
sku: variant.sku || "", sku: variant.sku || "",
ean: variant.ean || "",
upc: variant.upc || "",
barcode: variant.barcode || "",
manage_inventory: variant.manage_inventory || false, manage_inventory: variant.manage_inventory || false,
allow_backorder: variant.allow_backorder || false, allow_backorder: variant.allow_backorder || false,
inventory_kit: variant.inventory_kit || false, inventory_kit: variant.inventory_kit || false,
mid_code: variant.mid_code || "",
hs_code: variant.hs_code || "",
width: variant.width || "",
height: variant.height || "",
weight: variant.weight || "",
length: variant.length || "",
material: variant.material || "",
origin_country: variant.origin_country || "",
})) }))
} }
@@ -48,8 +48,8 @@ export const ProductDetail = () => {
</div> </div>
) )
})} })}
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start"> <div className="flex flex-col gap-x-4 gap-y-3 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2"> <div className="flex w-full flex-col gap-y-3">
<ProductGeneralSection product={product} /> <ProductGeneralSection product={product} />
<ProductMediaSection product={product} /> <ProductMediaSection product={product} />
<ProductOptionSection product={product} /> <ProductOptionSection product={product} />
@@ -65,7 +65,7 @@ export const ProductDetail = () => {
<JsonViewSection data={product} root="product" /> <JsonViewSection data={product} root="product" />
</div> </div>
</div> </div>
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[400px]"> <div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[400px]">
{sideBefore.widgets.map((w, i) => { {sideBefore.widgets.map((w, i) => {
return ( return (
<div key={i}> <div key={i}>
@@ -41,7 +41,7 @@ export const PromotionDetail = () => {
) )
})} })}
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start"> <div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2"> <div className="flex w-full flex-col gap-y-3">
<PromotionGeneralSection promotion={promotion} /> <PromotionGeneralSection promotion={promotion} />
<PromotionConditionsSection rules={rules || []} ruleType={"rules"} /> <PromotionConditionsSection rules={rules || []} ruleType={"rules"} />
<PromotionConditionsSection <PromotionConditionsSection
@@ -44,7 +44,7 @@ export const ReservationDetail = () => {
) )
})} })}
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start"> <div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2"> <div className="flex w-full flex-col gap-y-3">
<ReservationGeneralSection reservation={reservation} /> <ReservationGeneralSection reservation={reservation} />
{after.widgets.map((w, i) => { {after.widgets.map((w, i) => {
return ( return (
@@ -29,7 +29,7 @@ export const InLayout: Story = {
<div className="border-ui-border-base w-full max-w-[216px] border-r p-4"> <div className="border-ui-border-base w-full max-w-[216px] border-r p-4">
<Heading level="h3">Menubar</Heading> <Heading level="h3">Menubar</Heading>
</div> </div>
<div className="flex w-full flex-col gap-y-2 px-8 pb-8 pt-6"> <div className="flex w-full flex-col gap-y-3 px-8 pb-8 pt-6">
<Container> <Container>
<Heading>Section 1</Heading> <Heading>Section 1</Heading>
</Container> </Container>