feat(dashboard,ui,types,admin-shared): Add more extension zones + pass data to widgets (#7465)

This commit is contained in:
Kasper Fabricius Kristensen
2024-05-27 12:47:12 +02:00
committed by GitHub
parent 0b0e210f67
commit ab2e8fcd45
19 changed files with 220 additions and 109 deletions

View File

@@ -57,6 +57,7 @@
"@medusajs/admin-sdk": patch
"@medusajs/admin-shared": patch
"@medusajs/admin-vite-plugin": patch
"@medusajs/ui": patch
---
chore: Preview release changeset

View File

@@ -58,20 +58,17 @@ const PRODUCT_CATEGORY_INJECTION_ZONES = [
const PRICE_LIST_INJECTION_ZONES = [
"price_list.details.before",
"price_list.details.after",
"price_list.details.side.before",
"price_list.details.side.after",
"price_list.list.before",
"price_list.list.after",
] as const
const DISCOUNT_INJECTION_ZONES = [
"discount.details.before",
"discount.details.after",
"discount.list.before",
"discount.list.after",
] as const
const PROMOTION_INJECTION_ZONES = [
"promotion.details.before",
"promotion.details.after",
"promotion.details.side.before",
"promotion.details.side.after",
"promotion.list.before",
"promotion.list.after",
] as const
@@ -123,7 +120,6 @@ export const INJECTION_ZONES = [
...PRODUCT_COLLECTION_INJECTION_ZONES,
...PRODUCT_CATEGORY_INJECTION_ZONES,
...PRICE_LIST_INJECTION_ZONES,
...DISCOUNT_INJECTION_ZONES,
...PROMOTION_INJECTION_ZONES,
...GIFT_CARD_INJECTION_ZONES,
...REGION_INJECTION_ZONES,

View File

@@ -1,5 +1,7 @@
declare module "virtual:medusa/widgets/*" {
const widgets: { Component: () => JSX.Element }[]
import type { ComponentType } from "react"
const widgets: { Component: ComponentType<any> }[]
export default {
widgets,

View File

@@ -95,7 +95,6 @@ export const CreateCampaignForm = () => {
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button
size="small"
variant="primary"

View File

@@ -1,5 +1,4 @@
import {
clx,
CurrencyInput,
DatePicker,
Heading,
@@ -7,10 +6,12 @@ import {
RadioGroup,
Select,
Text,
Textarea,
} from "@medusajs/ui"
import { useEffect } from "react"
import { useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { Form } from "../../../../../components/common/form"
import { useStore } from "../../../../../hooks/api/store"
import { currencies, getCurrencySymbol } from "../../../../../lib/currencies"
@@ -70,24 +71,44 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
</Text>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name={`${fieldScope}name`}
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.name")}</Form.Label>
<div className="flex flex-col gap-y-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name={`${fieldScope}name`}
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.name")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name={`${fieldScope}campaign_identifier`}
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.identifier")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
<Form.Field
control={form.control}
@@ -98,7 +119,7 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
<Form.Label>{t("fields.description")}</Form.Label>
<Form.Control>
<Input {...field} />
<Textarea {...field} />
</Form.Control>
<Form.ErrorMessage />
@@ -106,27 +127,9 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
)
}}
/>
</div>
<Form.Field
control={form.control}
name={`${fieldScope}campaign_identifier`}
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("campaigns.fields.identifier")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<div></div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
control={form.control}
name={`${fieldScope}starts_at`}
@@ -198,20 +201,12 @@ export const CreateCampaignFormFields = ({ form, fieldScope = "" }) => {
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
className={clx("basis-1/2 border", {
"border border-ui-border-interactive":
"spend" === field.value,
})}
value={"spend"}
label={t("campaigns.budget.type.spend.title")}
description={t("campaigns.budget.type.spend.description")}
/>
<RadioGroup.ChoiceBox
className={clx("basis-1/2 border", {
"border border-ui-border-interactive":
"usage" === field.value,
})}
value={"usage"}
label={t("campaigns.budget.type.usage.title")}
description={t("campaigns.budget.type.usage.description")}

View File

@@ -39,7 +39,7 @@ export const CategoryDetail = () => {
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={product_category} />
</div>
)
})}
@@ -50,7 +50,7 @@ export const CategoryDetail = () => {
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={product_category} />
</div>
)
})}
@@ -62,7 +62,7 @@ export const CategoryDetail = () => {
{sideBefore.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={product_category} />
</div>
)
})}
@@ -70,7 +70,7 @@ export const CategoryDetail = () => {
{sideAfter.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={product_category} />
</div>
)
})}

View File

@@ -36,7 +36,7 @@ export const CollectionDetail = () => {
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={collection} />
</div>
)
})}
@@ -45,7 +45,7 @@ export const CollectionDetail = () => {
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={collection} />
</div>
)
})}

View File

@@ -36,7 +36,7 @@ export const CustomerGroupDetail = () => {
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={customer_group} />
</div>
)
})}
@@ -45,7 +45,7 @@ export const CustomerGroupDetail = () => {
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={customer_group} />
</div>
)
})}

View File

@@ -32,7 +32,7 @@ export const CustomerDetail = () => {
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={customer} />
</div>
)
})}
@@ -44,7 +44,7 @@ export const CustomerDetail = () => {
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={customer} />
</div>
)
})}

View File

@@ -43,7 +43,7 @@ export const OrderDetail = () => {
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={order} />
</div>
)
})}
@@ -56,7 +56,7 @@ export const OrderDetail = () => {
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={order} />
</div>
)
})}
@@ -68,7 +68,7 @@ export const OrderDetail = () => {
{sideBefore.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={order} />
</div>
)
})}
@@ -77,7 +77,7 @@ export const OrderDetail = () => {
{sideAfter.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={order} />
</div>
)
})}

View File

@@ -1,10 +1,16 @@
import { Outlet, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { usePriceList } from "../../../hooks/api/price-lists"
import { PricingConfigurationSection } from "./components/pricing-configuration-section"
import { PricingGeneralSection } from "./components/pricing-general-section"
import { PricingProductSection } from "./components/pricing-product-section"
import after from "virtual:medusa/widgets/price_list/details/after"
import before from "virtual:medusa/widgets/price_list/details/before"
import sideAfter from "virtual:medusa/widgets/price_list/details/side/after"
import sideBefore from "virtual:medusa/widgets/price_list/details/side/before"
export const PricingDetail = () => {
const { id } = useParams()
@@ -19,19 +25,51 @@ export const PricingDetail = () => {
}
return (
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2">
<PricingGeneralSection priceList={price_list} />
<PricingProductSection priceList={price_list} />
<div className="flex w-full flex-col gap-y-2 xl:hidden">
<PricingConfigurationSection priceList={price_list} />
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={price_list} />
</div>
)
})}
<div className="flex flex-col gap-x-4 lg:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2">
<PricingGeneralSection priceList={price_list} />
<PricingProductSection priceList={price_list} />
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={price_list} />
</div>
)
})}
<div className="hidden xl:block">
<JsonViewSection data={price_list} />
</div>
</div>
<JsonViewSection data={price_list} />
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[400px]">
{sideBefore.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={price_list} />
</div>
)
})}
<PricingConfigurationSection priceList={price_list} />
{sideAfter.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={price_list} />
</div>
)
})}
<div className="xl:hidden">
<JsonViewSection data={price_list} />
</div>
</div>
<Outlet />
</div>
<div className="hidden w-full max-w-[400px] flex-col gap-y-2 xl:flex">
<PricingConfigurationSection priceList={price_list} />
</div>
<Outlet />
</div>
)
}

View File

@@ -1,10 +1,28 @@
import { Outlet } from "react-router-dom"
import { PricingListTable } from "./components/pricing-list-table"
import after from "virtual:medusa/widgets/price_list/list/after"
import before from "virtual:medusa/widgets/price_list/list/before"
export const PricingList = () => {
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
<PricingListTable />
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
<Outlet />
</div>
)

View File

@@ -40,7 +40,7 @@ export const ProductDetail = () => {
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={product} />
</div>
)
})}
@@ -53,7 +53,7 @@ export const ProductDetail = () => {
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={product} />
</div>
)
})}
@@ -65,7 +65,7 @@ export const ProductDetail = () => {
{sideBefore.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={product} />
</div>
)
})}
@@ -75,7 +75,7 @@ export const ProductDetail = () => {
{sideAfter.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
<w.Component data={product} />
</div>
)
})}

View File

@@ -7,6 +7,9 @@ import { PromotionConditionsSection } from "./components/promotion-conditions-se
import { PromotionGeneralSection } from "./components/promotion-general-section"
import { promotionLoader } from "./loader"
import after from "virtual:medusa/widgets/promotion/details/after"
import before from "virtual:medusa/widgets/promotion/details/before"
export const PromotionDetail = () => {
const initialData = useLoaderData() as Awaited<
ReturnType<typeof promotionLoader>
@@ -24,33 +27,43 @@ export const PromotionDetail = () => {
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={promotion} />
</div>
)
})}
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
<div className="flex w-full flex-col gap-y-2">
<PromotionGeneralSection promotion={promotion} />
<PromotionConditionsSection rules={rules || []} ruleType={"rules"} />
<PromotionConditionsSection
rules={targetRules || []}
ruleType={"target-rules"}
/>
{promotion.type === "buyget" && (
<PromotionConditionsSection
rules={buyRules || []}
ruleType={"buy-rules"}
/>
)}
<div className="flex w-full flex-col gap-y-2 xl:hidden">
<CampaignSection campaign={promotion.campaign!} />
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component data={promotion} />
</div>
)
})}
<div className="hidden xl:block">
<JsonViewSection data={promotion} />
</div>
<JsonViewSection data={promotion as any} />
</div>
<div className="hidden w-full max-w-[400px] flex-col gap-y-2 xl:flex">
<div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 xl:mt-0 xl:max-w-[400px]">
<CampaignSection campaign={promotion.campaign!} />
<div className="xl:hidden">
<JsonViewSection data={promotion} />
</div>
</div>
</div>
<Outlet />

View File

@@ -1,9 +1,26 @@
import { PromotionListTable } from "./components/promotion-list-table"
import after from "virtual:medusa/widgets/promotion/list/after"
import before from "virtual:medusa/widgets/promotion/list/before"
export const PromotionsList = () => {
return (
<div className="flex flex-col gap-y-2">
{before.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
<PromotionListTable />
{after.widgets.map((w, i) => {
return (
<div key={i}>
<w.Component />
</div>
)
})}
</div>
)
}

View File

@@ -0,0 +1,27 @@
/**
* The props for detail widgets. All widgets that are rendered in a detail view,
* e.g. the product detail view, should implement this interface.
*
* The type of the data prop should be the same as the data that is passed to the widget.
* If the widget is rendered in the product detail view, the data prop should be of
* type AdminProduct.
*
* @example
* ```tsx
* import type { DetailWidgetProps, AdminProduct } from "@medusajs/types"
* import { defineConfig } from "@medusajs/admin-shared"
*
* const ProductWidget = ({ data }: DetailWidgetProps<AdminProduct>) => {
* return <div>{data.title}</div>
* }
*
* export const config = defineConfig({
* zone: "product.details.after",
* })
*
* export default ProductWidget
* ```
*/
export interface DetailWidgetProps<TData> {
data: TData
}

View File

@@ -0,0 +1 @@
export * from "./extensions"

View File

@@ -1,4 +1,5 @@
export * from "./address"
// export * from "./admin"
export * from "./api-key"
export * from "./auth"
export * from "./bundles"
@@ -19,6 +20,7 @@ export * from "./joiner"
export * from "./link-modules"
export * from "./logger"
export * from "./modules-sdk"
export * from "./notification"
export * from "./order"
export * from "./payment"
export * from "./pricing"
@@ -38,4 +40,3 @@ export * from "./transaction-base"
export * from "./user"
export * from "./workflow"
export * from "./workflows"
export * from "./notification"

View File

@@ -343,7 +343,7 @@ interface SingleProps extends PickerProps {
onChange?: (date: Date | undefined) => void
}
const SingleDatePicker = ({
const SingleDatePicker = React.forwardRef<HTMLButtonElement, SingleProps>(({
defaultValue,
value,
size = "base",
@@ -355,7 +355,7 @@ const SingleDatePicker = ({
placeholder,
translations,
...props
}: SingleProps) => {
}, ref) => {
const [open, setOpen] = React.useState(false)
const [date, setDate] = React.useState<Date | undefined>(
value ?? defaultValue ?? undefined
@@ -480,7 +480,7 @@ const SingleDatePicker = ({
}
return (
<Primitives.Root open={open} onOpenChange={onOpenChange}>
<Primitives.Root modal open={open} onOpenChange={onOpenChange}>
<Display
placeholder={placeholder}
disabled={disabled}
@@ -490,6 +490,7 @@ const SingleDatePicker = ({
aria-label={props["aria-label"]}
aria-labelledby={props["aria-labelledby"]}
size={size}
ref={ref}
>
{formattedDate}
</Display>
@@ -554,7 +555,7 @@ const SingleDatePicker = ({
</Flyout>
</Primitives.Root>
)
}
})
interface RangeProps extends PickerProps {
presets?: DateRangePreset[]
@@ -563,7 +564,7 @@ interface RangeProps extends PickerProps {
onChange?: (dateRange: DateRange | undefined) => void
}
const RangeDatePicker = ({
const RangeDatePicker = React.forwardRef<HTMLButtonElement, RangeProps>(({
/**
* The date range selected by default.
*/
@@ -587,7 +588,7 @@ const RangeDatePicker = ({
placeholder,
translations,
...props
}: RangeProps) => {
}, ref) => {
const [open, setOpen] = React.useState(false)
const [range, setRange] = React.useState<DateRange | undefined>(
value ?? defaultValue ?? undefined
@@ -756,7 +757,7 @@ const RangeDatePicker = ({
}
return (
<Primitives.Root open={open} onOpenChange={onOpenChange}>
<Primitives.Root modal open={open} onOpenChange={onOpenChange}>
<Display
placeholder={placeholder}
disabled={disabled}
@@ -766,6 +767,7 @@ const RangeDatePicker = ({
aria-label={props["aria-label"]}
aria-labelledby={props["aria-labelledby"]}
size={size}
ref={ref}
>
{displayRange}
</Display>
@@ -858,7 +860,7 @@ const RangeDatePicker = ({
</Flyout>
</Primitives.Root>
)
}
})
/**
* @interface
@@ -1031,22 +1033,23 @@ const validatePresets = (
* This component is based on the [Calendar](https://docs.medusajs.com/ui/components/calendar)
* component and [Radix UI Popover](https://www.radix-ui.com/primitives/docs/components/popover).
*/
const DatePicker = ({
const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(({
/**
* The date picker's mode.
*/
mode = "single",
...props
}: DatePickerProps) => {
}, ref) => {
if (props.presets) {
validatePresets(props.presets, props)
}
if (mode === "single") {
return <SingleDatePicker {...(props as SingleProps)} />
return <SingleDatePicker ref={ref} {...(props as SingleProps)} />
}
return <RangeDatePicker {...(props as RangeProps)} />
}
return <RangeDatePicker ref={ref} {...(props as RangeProps)} />
})
DatePicker.displayName = "DatePicker"
export { DatePicker }