feat(dashboard,icons,types,js-sdk): add providers to location UI (#8328)
This commit is contained in:
@@ -11,6 +11,7 @@ import { HttpTypes } from "@medusajs/types"
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/query-client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
import { fulfillmentProvidersQueryKeys } from "./fulfillment-providers"
|
||||
|
||||
const STOCK_LOCATIONS_QUERY_KEY = "stock_locations" as const
|
||||
export const stockLocationsQueryKeys = queryKeysFactory(
|
||||
@@ -175,3 +176,28 @@ export const useCreateStockLocationFulfillmentSet = (
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateStockLocationFulfillmentProviders = (
|
||||
id: string,
|
||||
options?: UseMutationOptions<
|
||||
HttpTypes.AdminStockLocationResponse,
|
||||
FetchError,
|
||||
HttpTypes.AdminBatchLink
|
||||
>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload) =>
|
||||
sdk.admin.stockLocation.updateFulfillmentProviders(id, payload),
|
||||
onSuccess: async (data, variables, context) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.details(),
|
||||
})
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: fulfillmentProvidersQueryKeys.all,
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
TextCell,
|
||||
TextHeader,
|
||||
} from "../../../components/table/table-cells/common/text-cell"
|
||||
import { formatProvider } from "../../../lib/format-provider"
|
||||
|
||||
const columnHelper = createColumnHelper<HttpTypes.AdminFulfillmentProvider>()
|
||||
|
||||
export const useFulfillmentProviderTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("id", {
|
||||
header: () => <TextHeader text={"Provider"} />,
|
||||
cell: ({ getValue }) => <TextCell text={formatProvider(getValue())} />,
|
||||
}),
|
||||
],
|
||||
[t]
|
||||
)
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useQueryParams } from "../../use-query-params"
|
||||
|
||||
type UseFulfillmentProviderTableQueryProps = {
|
||||
prefix?: string
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const useFulfillmentProvidersTableQuery = ({
|
||||
prefix,
|
||||
pageSize = 20,
|
||||
}: UseFulfillmentProviderTableQueryProps) => {
|
||||
const queryObject = useQueryParams(
|
||||
["offset", "q", "stock_location_id"],
|
||||
prefix
|
||||
)
|
||||
|
||||
const { offset, q, stock_location_id } = queryObject
|
||||
|
||||
const searchParams: HttpTypes.AdminFulfillmentProviderListParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
stock_location_id,
|
||||
q,
|
||||
}
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
raw: queryObject,
|
||||
}
|
||||
}
|
||||
@@ -1020,6 +1020,15 @@
|
||||
"delete": {
|
||||
"confirmation": "You are about to delete the stock location {{name}}. This action cannot be undone."
|
||||
},
|
||||
"fulfillmentProviders": {
|
||||
"header": "Fulfillment Providers",
|
||||
"shippingOptionsTooltip": "This dropdown will only consist of providers enabled for this location. Add them to the location if the dropdown is disabled.",
|
||||
"label": "Connected fulfillment providers",
|
||||
"connectedTo": "Connected to {{count}} of {{total}} fulfillment providers",
|
||||
"noProviders": "This Stock Location is not connected to any fulfillment providers.",
|
||||
"action": "Connect Providers",
|
||||
"successToast": "Fulfillment providers for stock location were successfully updated."
|
||||
},
|
||||
"fulfillmentSets": {
|
||||
"pickup": {
|
||||
"header": "Pickup"
|
||||
|
||||
@@ -871,6 +871,13 @@ export const RouteMap: RouteObject[] = [
|
||||
lazy: () =>
|
||||
import("../../routes/locations/location-sales-channels"),
|
||||
},
|
||||
{
|
||||
path: "fulfillment-providers",
|
||||
lazy: () =>
|
||||
import(
|
||||
"../../routes/locations/location-fulfillment-providers"
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "fulfillment-set/:fset_id",
|
||||
children: [
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./location-fulfillment-providers-section"
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { HandTruck, PencilSquare } from "@medusajs/icons"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { NoRecords } from "../../../../../components/common/empty-table-content"
|
||||
import { IconAvatar } from "../../../../../components/common/icon-avatar"
|
||||
import { useFulfillmentProviders } from "../../../../../hooks/api"
|
||||
import { formatProvider } from "../../../../../lib/format-provider"
|
||||
|
||||
type LocationsFulfillmentProvidersSectionProps = {
|
||||
location: HttpTypes.AdminStockLocation
|
||||
}
|
||||
|
||||
function LocationsFulfillmentProvidersSection({
|
||||
location,
|
||||
}: LocationsFulfillmentProvidersSectionProps) {
|
||||
const { t } = useTranslation()
|
||||
const { fulfillment_providers } = useFulfillmentProviders({
|
||||
stock_location_id: location.id,
|
||||
fields: "id",
|
||||
})
|
||||
|
||||
return (
|
||||
<Container className="flex flex-col px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Heading level="h2">
|
||||
{t("stockLocations.fulfillmentProviders.header")}
|
||||
</Heading>
|
||||
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: "fulfillment-providers",
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{fulfillment_providers?.length ? (
|
||||
<div className="flex flex-col gap-y-4 pt-4">
|
||||
<div className="grid grid-cols-[28px_1fr] items-center gap-x-3 gap-y-3">
|
||||
{fulfillment_providers?.map((fulfillmentProvider) => {
|
||||
return (
|
||||
<>
|
||||
<IconAvatar>
|
||||
<HandTruck className="text-ui-fg-subtle" />
|
||||
</IconAvatar>
|
||||
|
||||
<div className="txt-compact-small">
|
||||
{formatProvider(fulfillmentProvider.id)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<NoRecords
|
||||
className="h-fit pb-2 pt-6 text-center"
|
||||
action={{
|
||||
label: t("stockLocations.fulfillmentProviders.action"),
|
||||
to: "fulfillment-providers",
|
||||
}}
|
||||
message={t("stockLocations.fulfillmentProviders.noProviders")}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default LocationsFulfillmentProvidersSection
|
||||
@@ -1,2 +1,2 @@
|
||||
export const detailsFields =
|
||||
"name,*sales_channels,*address,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones.geo_zones,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.rules,*fulfillment_sets.service_zones.shipping_options.shipping_profile"
|
||||
"name,*sales_channels,*address,fulfillment_sets.type,fulfillment_sets.name,*fulfillment_sets.service_zones.geo_zones,*fulfillment_sets.service_zones,*fulfillment_sets.service_zones.shipping_options,*fulfillment_sets.service_zones.shipping_options.rules,*fulfillment_sets.service_zones.shipping_options.shipping_profile,*fulfillment_providers"
|
||||
|
||||
+5
-9
@@ -10,6 +10,7 @@ import after from "virtual:medusa/widgets/location/details/after"
|
||||
import before from "virtual:medusa/widgets/location/details/before"
|
||||
import sideAfter from "virtual:medusa/widgets/location/details/side/after"
|
||||
import sideBefore from "virtual:medusa/widgets/location/details/side/before"
|
||||
import LocationsFulfillmentProvidersSection from "./components/location-fulfillment-providers-section/location-fulfillment-providers-section"
|
||||
import { detailsFields } from "./const"
|
||||
|
||||
export const LocationDetail = () => {
|
||||
@@ -23,15 +24,7 @@ export const LocationDetail = () => {
|
||||
isPending: isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useStockLocation(
|
||||
location_id!,
|
||||
{
|
||||
fields: detailsFields,
|
||||
},
|
||||
{
|
||||
initialData,
|
||||
}
|
||||
)
|
||||
} = useStockLocation(location_id!, { fields: detailsFields }, { initialData })
|
||||
|
||||
// TODO: Move to loading.tsx and set as Suspense fallback for the route
|
||||
if (isLoading || !location) {
|
||||
@@ -73,7 +66,10 @@ export const LocationDetail = () => {
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<LocationsSalesChannelsSection location={location} />
|
||||
<LocationsFulfillmentProvidersSection location={location} />
|
||||
|
||||
{sideAfter.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Button, Checkbox, toast } from "@medusajs/ui"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/modals"
|
||||
import { DataTable } from "../../../../../components/table/data-table"
|
||||
import { useFulfillmentProviders } from "../../../../../hooks/api/fulfillment-providers"
|
||||
import { useUpdateStockLocationFulfillmentProviders } from "../../../../../hooks/api/stock-locations"
|
||||
import { useFulfillmentProviderTableColumns } from "../../../../../hooks/table/columns/use-fulfillment-provider-table-columns"
|
||||
import { useDateTableFilters } from "../../../../../hooks/table/filters"
|
||||
import { useFulfillmentProvidersTableQuery } from "../../../../../hooks/table/query/use-fulfillment-providers-table-query"
|
||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||
|
||||
type LocationEditFulfillmentProvidersFormProps = {
|
||||
location: HttpTypes.AdminStockLocation
|
||||
}
|
||||
|
||||
const EditFulfillmentProvidersFormSchema = zod.object({
|
||||
fulfillment_providers: zod.array(zod.string()).optional(),
|
||||
})
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export const LocationEditFulfillmentProvidersForm = ({
|
||||
location,
|
||||
}: LocationEditFulfillmentProvidersFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<zod.infer<typeof EditFulfillmentProvidersFormSchema>>({
|
||||
defaultValues: {
|
||||
fulfillment_providers:
|
||||
location.fulfillment_providers?.map((fp) => fp.id) ?? [],
|
||||
},
|
||||
resolver: zodResolver(EditFulfillmentProvidersFormSchema),
|
||||
})
|
||||
|
||||
const { setValue } = form
|
||||
|
||||
const initialState =
|
||||
location.fulfillment_providers?.reduce((acc, curr) => {
|
||||
acc[curr.id] = true
|
||||
return acc
|
||||
}, {} as RowSelectionState) ?? {}
|
||||
|
||||
const [rowSelection, setRowSelection] =
|
||||
useState<RowSelectionState>(initialState)
|
||||
|
||||
useEffect(() => {
|
||||
const ids = Object.keys(rowSelection)
|
||||
setValue("fulfillment_providers", ids, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
})
|
||||
}, [rowSelection, setValue])
|
||||
|
||||
const { searchParams, raw } = useFulfillmentProvidersTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const { fulfillment_providers, count, isLoading, isError, error } =
|
||||
useFulfillmentProviders(
|
||||
{ ...searchParams },
|
||||
{ placeholderData: keepPreviousData }
|
||||
)
|
||||
|
||||
const filters = useDateTableFilters()
|
||||
const columns = useColumns()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: fulfillment_providers ?? [],
|
||||
columns,
|
||||
count,
|
||||
enablePagination: true,
|
||||
enableRowSelection: true,
|
||||
rowSelection: {
|
||||
state: rowSelection,
|
||||
updater: setRowSelection,
|
||||
},
|
||||
getRowId: (row) => row.id,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const { mutateAsync, isPending: isMutating } =
|
||||
useUpdateStockLocationFulfillmentProviders(location.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
const originalIds = location.fulfillment_providers?.map((sc) => sc.id)
|
||||
|
||||
const arr = data.fulfillment_providers ?? []
|
||||
|
||||
await mutateAsync(
|
||||
{
|
||||
add: arr.filter((i) => !originalIds?.includes(i)),
|
||||
remove: originalIds?.filter((i) => !arr.includes(i)),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t("stockLocations.fulfillmentProviders.successToast"))
|
||||
handleSuccess(`/settings/locations/${location.id}`)
|
||||
},
|
||||
onError: (e) => {
|
||||
toast.error(e.message)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<RouteFocusModal.Body>
|
||||
<DataTable
|
||||
table={table}
|
||||
columns={columns}
|
||||
pageSize={PAGE_SIZE}
|
||||
isLoading={isLoading}
|
||||
count={count}
|
||||
filters={filters}
|
||||
search
|
||||
pagination
|
||||
orderBy={["id", "created_at", "updated_at"]}
|
||||
queryObject={raw}
|
||||
layout="fill"
|
||||
/>
|
||||
</RouteFocusModal.Body>
|
||||
|
||||
<RouteFocusModal.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteFocusModal.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteFocusModal.Close>
|
||||
|
||||
<Button size="small" isLoading={isMutating} onClick={handleSubmit}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteFocusModal.Footer>
|
||||
</div>
|
||||
</RouteFocusModal.Form>
|
||||
)
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<HttpTypes.AdminFulfillmentProvider>()
|
||||
|
||||
const useColumns = () => {
|
||||
const columns = useFulfillmentProviderTableColumns()
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsSomePageRowsSelected()
|
||||
? "indeterminate"
|
||||
: table.getIsAllPageRowsSelected()
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}),
|
||||
...columns,
|
||||
],
|
||||
[columns]
|
||||
)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./edit-fulfillment-providers-form"
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { LocationFulfillmentProviders as Component } from "./location-fulfillment-providers"
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { RouteFocusModal } from "../../../components/modals"
|
||||
import { useStockLocation } from "../../../hooks/api/stock-locations"
|
||||
import { LocationEditFulfillmentProvidersForm } from "./components/edit-fulfillment-providers-form"
|
||||
|
||||
export const LocationFulfillmentProviders = () => {
|
||||
const { location_id } = useParams()
|
||||
const { stock_location, isPending, isError, error } = useStockLocation(
|
||||
location_id!,
|
||||
{ fields: "id,*fulfillment_providers" }
|
||||
)
|
||||
|
||||
const ready = !isPending && !!stock_location
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteFocusModal>
|
||||
{ready && (
|
||||
<LocationEditFulfillmentProvidersForm location={stock_location} />
|
||||
)}
|
||||
</RouteFocusModal>
|
||||
)
|
||||
}
|
||||
+12
-2
@@ -17,12 +17,14 @@ type CreateShippingOptionDetailsFormProps = {
|
||||
form: UseFormReturn<CreateShippingOptionSchema>
|
||||
isReturn?: boolean
|
||||
zone: HttpTypes.AdminServiceZone
|
||||
locationId: string
|
||||
}
|
||||
|
||||
export const CreateShippingOptionDetailsForm = ({
|
||||
form,
|
||||
isReturn = false,
|
||||
zone,
|
||||
locationId,
|
||||
}: CreateShippingOptionDetailsFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -37,7 +39,11 @@ export const CreateShippingOptionDetailsForm = ({
|
||||
})
|
||||
|
||||
const fulfillmentProviders = useComboboxData({
|
||||
queryFn: (params) => sdk.admin.fulfillmentProvider.list(params),
|
||||
queryFn: (params) =>
|
||||
sdk.admin.fulfillmentProvider.list({
|
||||
...params,
|
||||
stock_location_id: locationId,
|
||||
}),
|
||||
queryKey: ["fulfillment_providers"],
|
||||
getOptions: (data) =>
|
||||
data.fulfillment_providers.map((provider) => ({
|
||||
@@ -161,7 +167,11 @@ export const CreateShippingOptionDetailsForm = ({
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
<Form.Label
|
||||
tooltip={t(
|
||||
"stockLocations.fulfillmentProviders.shippingOptionsTooltip"
|
||||
)}
|
||||
>
|
||||
{t("stockLocations.shippingOptions.fields.provider")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
|
||||
+1
@@ -257,6 +257,7 @@ export function CreateShippingOptionsForm({
|
||||
form={form}
|
||||
zone={zone}
|
||||
isReturn={isReturn}
|
||||
locationId={locationId}
|
||||
/>
|
||||
</ProgressTabs.Content>
|
||||
<ProgressTabs.Content value={Tab.PRICING} className="size-full">
|
||||
|
||||
+10
-2
@@ -49,7 +49,11 @@ export const EditShippingOptionForm = ({
|
||||
})
|
||||
|
||||
const fulfillmentProviders = useComboboxData({
|
||||
queryFn: (params) => sdk.admin.fulfillmentProvider.list(params),
|
||||
queryFn: (params) =>
|
||||
sdk.admin.fulfillmentProvider.list({
|
||||
...params,
|
||||
stock_location_id: locationId,
|
||||
}),
|
||||
queryKey: ["fulfillment_providers"],
|
||||
getOptions: (data) =>
|
||||
data.fulfillment_providers.map((provider) => ({
|
||||
@@ -210,7 +214,11 @@ export const EditShippingOptionForm = ({
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
<Form.Label
|
||||
tooltip={t(
|
||||
"stockLocations.fulfillmentProviders.shippingOptionsTooltip"
|
||||
)}
|
||||
>
|
||||
{t("stockLocations.shippingOptions.fields.provider")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
|
||||
Reference in New Issue
Block a user