feat(dashboard): refactor location list UI to use data table (#13571)
* wip: convert location list to a table * chore: changeset * fix: rm search bluring on loading change * feat: translations and palceholders, cleanup, make content more compact * fix: delete message * chore: optimise use memo * fix: update toast * feat: make stock location address searchable * fix: search input blur on load finish
This commit is contained in:
1 parent
6e806942c7
commit
10787c865f
15 files changed
+373
-134
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(dashboard): refactor location list UI to use data table
|
||||||
@@ -31,26 +31,26 @@ type DataTableActionProps = {
|
|||||||
label: string
|
label: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
to: string
|
to: string
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type DataTableActionMenuActionProps = {
|
type DataTableActionMenuActionProps = {
|
||||||
label: string
|
label: string
|
||||||
icon: ReactNode
|
icon: ReactNode
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
to: string
|
to: string
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type DataTableActionMenuGroupProps = {
|
type DataTableActionMenuGroupProps = {
|
||||||
actions: DataTableActionMenuActionProps[]
|
actions: DataTableActionMenuActionProps[]
|
||||||
@@ -138,15 +138,18 @@ export const DataTable = <TData,>({
|
|||||||
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
||||||
|
|
||||||
// If view config is disabled, don't use column visibility features
|
// If view config is disabled, don't use column visibility features
|
||||||
const effectiveEnableColumnVisibility = isViewConfigEnabled && enableColumnVisibility
|
const effectiveEnableColumnVisibility =
|
||||||
|
isViewConfigEnabled && enableColumnVisibility
|
||||||
const effectiveEnableViewSelector = isViewConfigEnabled && enableViewSelector
|
const effectiveEnableViewSelector = isViewConfigEnabled && enableViewSelector
|
||||||
|
|
||||||
const enableFiltering = filters && filters.length > 0
|
const enableFiltering = filters && filters.length > 0
|
||||||
const showFilterMenu = enableFilterMenu !== undefined ? enableFilterMenu : enableFiltering
|
const showFilterMenu =
|
||||||
|
enableFilterMenu !== undefined ? enableFilterMenu : enableFiltering
|
||||||
const enableCommands = commands && commands.length > 0
|
const enableCommands = commands && commands.length > 0
|
||||||
const enableSorting = columns.some((column) => column.enableSorting)
|
const enableSorting = columns.some((column) => column.enableSorting)
|
||||||
|
|
||||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(initialColumnVisibility)
|
const [columnVisibility, setColumnVisibility] =
|
||||||
|
React.useState<VisibilityState>(initialColumnVisibility)
|
||||||
|
|
||||||
// Update column visibility when initial visibility changes
|
// Update column visibility when initial visibility changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -154,9 +157,12 @@ export const DataTable = <TData,>({
|
|||||||
const currentKeys = Object.keys(columnVisibility).sort()
|
const currentKeys = Object.keys(columnVisibility).sort()
|
||||||
const newKeys = Object.keys(initialColumnVisibility).sort()
|
const newKeys = Object.keys(initialColumnVisibility).sort()
|
||||||
|
|
||||||
const hasChanged = currentKeys.length !== newKeys.length ||
|
const hasChanged =
|
||||||
|
currentKeys.length !== newKeys.length ||
|
||||||
currentKeys.some((key, index) => key !== newKeys[index]) ||
|
currentKeys.some((key, index) => key !== newKeys[index]) ||
|
||||||
Object.entries(initialColumnVisibility).some(([key, value]) => columnVisibility[key] !== value)
|
Object.entries(initialColumnVisibility).some(
|
||||||
|
([key, value]) => columnVisibility[key] !== value
|
||||||
|
)
|
||||||
|
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
setColumnVisibility(initialColumnVisibility)
|
setColumnVisibility(initialColumnVisibility)
|
||||||
@@ -164,10 +170,13 @@ export const DataTable = <TData,>({
|
|||||||
}, [initialColumnVisibility])
|
}, [initialColumnVisibility])
|
||||||
|
|
||||||
// Wrapper function to handle column visibility changes
|
// Wrapper function to handle column visibility changes
|
||||||
const handleColumnVisibilityChange = React.useCallback((visibility: VisibilityState) => {
|
const handleColumnVisibilityChange = React.useCallback(
|
||||||
setColumnVisibility(visibility)
|
(visibility: VisibilityState) => {
|
||||||
onColumnVisibilityChange?.(visibility)
|
setColumnVisibility(visibility)
|
||||||
}, [onColumnVisibilityChange])
|
onColumnVisibilityChange?.(visibility)
|
||||||
|
},
|
||||||
|
[onColumnVisibilityChange]
|
||||||
|
)
|
||||||
|
|
||||||
// Extract filter IDs for query param management
|
// Extract filter IDs for query param management
|
||||||
const filterIds = useMemo(() => filters?.map((f) => f.id) ?? [], [filters])
|
const filterIds = useMemo(() => filters?.map((f) => f.id) ?? [], [filters])
|
||||||
@@ -231,7 +240,7 @@ export const DataTable = <TData,>({
|
|||||||
Array.from(prev.keys()).forEach((key) => {
|
Array.from(prev.keys()).forEach((key) => {
|
||||||
if (prefixedFilterIds.includes(key)) {
|
if (prefixedFilterIds.includes(key)) {
|
||||||
// Extract the unprefixed key
|
// Extract the unprefixed key
|
||||||
const unprefixedKey = prefix ? key.replace(`${prefix}_`, '') : key
|
const unprefixedKey = prefix ? key.replace(`${prefix}_`, "") : key
|
||||||
if (!(unprefixedKey in value)) {
|
if (!(unprefixedKey in value)) {
|
||||||
prev.delete(key)
|
prev.delete(key)
|
||||||
}
|
}
|
||||||
@@ -257,11 +266,14 @@ export const DataTable = <TData,>({
|
|||||||
}, [order])
|
}, [order])
|
||||||
|
|
||||||
// Memoize current configuration to prevent infinite loops
|
// Memoize current configuration to prevent infinite loops
|
||||||
const currentConfiguration = useMemo(() => ({
|
const currentConfiguration = useMemo(
|
||||||
filters: filtering,
|
() => ({
|
||||||
sorting: sorting,
|
filters: filtering,
|
||||||
search: search,
|
sorting: sorting,
|
||||||
}), [filtering, sorting, search])
|
search: search,
|
||||||
|
}),
|
||||||
|
[filtering, sorting, search]
|
||||||
|
)
|
||||||
|
|
||||||
const handleSortingChange = (value: DataTableSortingState) => {
|
const handleSortingChange = (value: DataTableSortingState) => {
|
||||||
setSearchParams((prev) => {
|
setSearchParams((prev) => {
|
||||||
@@ -315,42 +327,43 @@ export const DataTable = <TData,>({
|
|||||||
onRowClick: rowHref ? onRowClick : undefined,
|
onRowClick: rowHref ? onRowClick : undefined,
|
||||||
pagination: enablePagination
|
pagination: enablePagination
|
||||||
? {
|
? {
|
||||||
state: pagination,
|
state: pagination,
|
||||||
onPaginationChange: handlePaginationChange,
|
onPaginationChange: handlePaginationChange,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
filtering: enableFiltering
|
filtering: enableFiltering
|
||||||
? {
|
? {
|
||||||
state: filtering,
|
state: filtering,
|
||||||
onFilteringChange: handleFilteringChange,
|
onFilteringChange: handleFilteringChange,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
sorting: enableSorting
|
sorting: enableSorting
|
||||||
? {
|
? {
|
||||||
state: sorting,
|
state: sorting,
|
||||||
onSortingChange: handleSortingChange,
|
onSortingChange: handleSortingChange,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
search: enableSearch
|
search: enableSearch
|
||||||
? {
|
? {
|
||||||
state: search,
|
state: search,
|
||||||
onSearchChange: handleSearchChange,
|
onSearchChange: handleSearchChange,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
isLoading,
|
isLoading,
|
||||||
columnVisibility: effectiveEnableColumnVisibility
|
columnVisibility: effectiveEnableColumnVisibility
|
||||||
? {
|
? {
|
||||||
state: columnVisibility,
|
state: columnVisibility,
|
||||||
onColumnVisibilityChange: handleColumnVisibilityChange,
|
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||||
}
|
}
|
||||||
: undefined,
|
|
||||||
columnOrder: effectiveEnableColumnVisibility && columnOrder && onColumnOrderChange
|
|
||||||
? {
|
|
||||||
state: columnOrder,
|
|
||||||
onColumnOrderChange: onColumnOrderChange,
|
|
||||||
}
|
|
||||||
: undefined,
|
: undefined,
|
||||||
|
columnOrder:
|
||||||
|
effectiveEnableColumnVisibility && columnOrder && onColumnOrderChange
|
||||||
|
? {
|
||||||
|
state: columnOrder,
|
||||||
|
onColumnOrderChange: onColumnOrderChange,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const shouldRenderHeading = heading || subHeading
|
const shouldRenderHeading = heading || subHeading
|
||||||
@@ -358,7 +371,9 @@ export const DataTable = <TData,>({
|
|||||||
return (
|
return (
|
||||||
<UiDataTable
|
<UiDataTable
|
||||||
instance={instance}
|
instance={instance}
|
||||||
className={layout === "fill" ? "h-full [&_tr]:last-of-type:!border-b" : undefined}
|
className={
|
||||||
|
layout === "fill" ? "h-full [&_tr]:last-of-type:!border-b" : undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<UiDataTable.Toolbar
|
<UiDataTable.Toolbar
|
||||||
className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"
|
className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"
|
||||||
@@ -397,7 +412,9 @@ export const DataTable = <TData,>({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{actionMenu && <ActionMenu variant="primary" {...actionMenu} />}
|
{actionMenu && <ActionMenu variant="primary" {...actionMenu} />}
|
||||||
{actions && actions.length > 0 && <DataTableActions actions={actions} />}
|
{actions && actions.length > 0 && (
|
||||||
|
<DataTableActions actions={actions} />
|
||||||
|
)}
|
||||||
{!actions && action && <DataTableAction {...action} />}
|
{!actions && action && <DataTableAction {...action} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -407,7 +424,9 @@ export const DataTable = <TData,>({
|
|||||||
<UiDataTable.Pagination translations={paginationTranslations} />
|
<UiDataTable.Pagination translations={paginationTranslations} />
|
||||||
)}
|
)}
|
||||||
{enableCommands && (
|
{enableCommands && (
|
||||||
<UiDataTable.CommandBar selectedLabel={(count) => `${count} selected`} />
|
<UiDataTable.CommandBar
|
||||||
|
selectedLabel={(count) => `${count} selected`}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</UiDataTable>
|
</UiDataTable>
|
||||||
)
|
)
|
||||||
@@ -520,4 +539,3 @@ const DataTableActions = ({ actions }: { actions: DataTableActionProps[] }) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5834,9 +5834,23 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"noRecordsMessage": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"noRecordsMessageEmpty": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"noRecordsMessageFiltered": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["description"],
|
"required": [
|
||||||
|
"description",
|
||||||
|
"noRecordsMessage",
|
||||||
|
"noRecordsMessageEmpty",
|
||||||
|
"noRecordsMessageFiltered"
|
||||||
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
@@ -5876,9 +5890,12 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"confirmation": {
|
"confirmation": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"successToast": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["confirmation"],
|
"required": ["confirmation", "successToast"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"fulfillmentProviders": {
|
"fulfillmentProviders": {
|
||||||
|
|||||||
@@ -1535,7 +1535,10 @@
|
|||||||
"stockLocations": {
|
"stockLocations": {
|
||||||
"domain": "Locations & Shipping",
|
"domain": "Locations & Shipping",
|
||||||
"list": {
|
"list": {
|
||||||
"description": "Manage your store's stock locations and shipping options."
|
"description": "Manage your store's stock locations and shipping options.",
|
||||||
|
"noRecordsMessage": "No records",
|
||||||
|
"noRecordsMessageEmpty": "No locations found",
|
||||||
|
"noRecordsMessageFiltered": "No locations found matching the filters"
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"header": "Create Stock Location",
|
"header": "Create Stock Location",
|
||||||
@@ -1548,7 +1551,8 @@
|
|||||||
"successToast": "Location {{name}} was successfully updated."
|
"successToast": "Location {{name}} was successfully updated."
|
||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"confirmation": "You are about to delete the stock location \"{{name}}\". This action cannot be undone."
|
"confirmation": "You are about to delete the stock location \"{{name}}\". This action cannot be undone.",
|
||||||
|
"successToast": "Location \"{{name}}\" was successfully deleted."
|
||||||
},
|
},
|
||||||
"fulfillmentProviders": {
|
"fulfillmentProviders": {
|
||||||
"header": "Fulfillment Providers",
|
"header": "Fulfillment Providers",
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t("stockLocations.edit.successToast"))
|
toast.success(t("stockLocations.edit.successToast", { name: name }))
|
||||||
handleSuccess()
|
handleSuccess()
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
export * from "./location-list-header"
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
import { Button, Container, Heading, Text } from "@medusajs/ui"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { Link } from "react-router-dom"
|
|
||||||
|
|
||||||
export const LocationListHeader = () => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="flex h-fit items-center justify-between gap-x-4 px-6 py-4">
|
|
||||||
<div>
|
|
||||||
<Heading>{t("stockLocations.domain")}</Heading>
|
|
||||||
<Text className="text-ui-fg-subtle txt-small">
|
|
||||||
{t("stockLocations.list.description")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Button size="small" className="shrink-0" variant="secondary" asChild>
|
|
||||||
<Link to="create">{t("actions.create")}</Link>
|
|
||||||
</Button>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
export { shippingListLoader as loader } from "./loader"
|
|
||||||
export { LocationList as Component } from "./location-list"
|
export { LocationList as Component } from "./location-list"
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { FetchError } from "@medusajs/js-sdk"
|
|
||||||
import { LoaderFunctionArgs, redirect } from "react-router-dom"
|
|
||||||
|
|
||||||
import { HttpTypes } from "@medusajs/types"
|
|
||||||
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
|
||||||
import { sdk } from "../../../lib/client"
|
|
||||||
import { queryClient } from "../../../lib/query-client"
|
|
||||||
import { LOCATION_LIST_FIELDS } from "./constants"
|
|
||||||
|
|
||||||
const shippingListQuery = () => ({
|
|
||||||
queryKey: stockLocationsQueryKeys.lists(),
|
|
||||||
queryFn: async () => {
|
|
||||||
return await sdk.admin.stockLocation
|
|
||||||
.list({
|
|
||||||
// TODO: change this when RQ is fixed
|
|
||||||
fields: LOCATION_LIST_FIELDS,
|
|
||||||
})
|
|
||||||
.catch((error: FetchError) => {
|
|
||||||
if (error.status === 401) {
|
|
||||||
throw redirect("/login")
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const shippingListLoader = async (_: LoaderFunctionArgs) => {
|
|
||||||
const query = shippingListQuery()
|
|
||||||
|
|
||||||
return (
|
|
||||||
queryClient.getQueryData<HttpTypes.AdminStockLocationListResponse>(
|
|
||||||
query.queryKey
|
|
||||||
) ?? (await queryClient.fetchQuery(query))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,46 @@
|
|||||||
import { ShoppingBag, TruckFast } from "@medusajs/icons"
|
import { ShoppingBag, TruckFast } from "@medusajs/icons"
|
||||||
import { Container, Heading } from "@medusajs/ui"
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useLoaderData } from "react-router-dom"
|
|
||||||
|
|
||||||
import { useStockLocations } from "../../../hooks/api/stock-locations"
|
import { useStockLocations } from "../../../hooks/api/stock-locations"
|
||||||
import LocationListItem from "./components/location-list-item/location-list-item"
|
|
||||||
import { LOCATION_LIST_FIELDS } from "./constants"
|
import { LOCATION_LIST_FIELDS } from "./constants"
|
||||||
import { shippingListLoader } from "./loader"
|
import { useLocationListTableColumns } from "./use-location-list-table-columns"
|
||||||
|
import { useLocationListTableQuery } from "./use-location-list-table-query"
|
||||||
|
|
||||||
|
import { DataTable } from "../../../components/data-table"
|
||||||
import { SidebarLink } from "../../../components/common/sidebar-link/sidebar-link"
|
import { SidebarLink } from "../../../components/common/sidebar-link/sidebar-link"
|
||||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||||
import { useExtension } from "../../../providers/extension-provider"
|
import { useExtension } from "../../../providers/extension-provider"
|
||||||
import { LocationListHeader } from "./components/location-list-header"
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
const PREFIX = "loc"
|
||||||
|
|
||||||
export function LocationList() {
|
export function LocationList() {
|
||||||
const initialData = useLoaderData() as Awaited<
|
const { t } = useTranslation()
|
||||||
ReturnType<typeof shippingListLoader>
|
|
||||||
>
|
const searchParams = useLocationListTableQuery({
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
prefix: PREFIX,
|
||||||
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
stock_locations: stockLocations = [],
|
stock_locations: stockLocations = [],
|
||||||
|
count,
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
|
isLoading,
|
||||||
} = useStockLocations(
|
} = useStockLocations(
|
||||||
{
|
{
|
||||||
fields: LOCATION_LIST_FIELDS,
|
fields: LOCATION_LIST_FIELDS,
|
||||||
|
...searchParams,
|
||||||
},
|
},
|
||||||
{ initialData }
|
{
|
||||||
|
placeholderData: keepPreviousData,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const columns = useLocationListTableColumns()
|
||||||
const { getWidgets } = useExtension()
|
const { getWidgets } = useExtension()
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
@@ -46,12 +58,38 @@ export function LocationList() {
|
|||||||
showJSON
|
showJSON
|
||||||
>
|
>
|
||||||
<TwoColumnPage.Main>
|
<TwoColumnPage.Main>
|
||||||
<LocationListHeader />
|
<Container className="flex flex-col divide-y p-0">
|
||||||
<div className="flex flex-col gap-3 lg:col-span-2">
|
<DataTable
|
||||||
{stockLocations.map((location) => (
|
data={stockLocations}
|
||||||
<LocationListItem key={location.id} location={location} />
|
columns={columns}
|
||||||
))}
|
rowCount={count}
|
||||||
</div>
|
pageSize={PAGE_SIZE}
|
||||||
|
getRowId={(row) => row.id}
|
||||||
|
heading={t("stockLocations.domain")}
|
||||||
|
subHeading={t("stockLocations.list.description")}
|
||||||
|
emptyState={{
|
||||||
|
empty: {
|
||||||
|
heading: t("stockLocations.list.noRecordsMessage"),
|
||||||
|
description: t("stockLocations.list.noRecordsMessageEmpty"),
|
||||||
|
},
|
||||||
|
filtered: {
|
||||||
|
heading: t("stockLocations.list.noRecordsMessage"),
|
||||||
|
description: t("stockLocations.list.noRecordsMessageFiltered"),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: t("actions.create"),
|
||||||
|
to: "create",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
isLoading={isLoading}
|
||||||
|
rowHref={(row) => `/settings/locations/${row.id}`}
|
||||||
|
enableSearch={true}
|
||||||
|
prefix={PREFIX}
|
||||||
|
layout="fill"
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
</TwoColumnPage.Main>
|
</TwoColumnPage.Main>
|
||||||
<TwoColumnPage.Sidebar>
|
<TwoColumnPage.Sidebar>
|
||||||
<LinksSection />
|
<LinksSection />
|
||||||
|
|||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import {
|
||||||
|
createDataTableColumnHelper,
|
||||||
|
StatusBadge,
|
||||||
|
toast,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
|
|
||||||
|
import { PlaceholderCell } from "../../../components/table/table-cells/common/placeholder-cell"
|
||||||
|
import { getFormattedAddress } from "../../../lib/addresses"
|
||||||
|
import { FulfillmentSetType } from "../common/constants"
|
||||||
|
import { queryClient } from "../../../lib/query-client"
|
||||||
|
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
||||||
|
import { ListSummary } from "../../../components/common/list-summary"
|
||||||
|
import { sdk } from "../../../lib/client"
|
||||||
|
|
||||||
|
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminStockLocation>()
|
||||||
|
|
||||||
|
export const useLocationListTableColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const handleDelete = async (location: HttpTypes.AdminStockLocation) => {
|
||||||
|
const result = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("stockLocations.delete.confirmation", {
|
||||||
|
name: location.name,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.remove"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sdk.admin.stockLocation.delete(location.id)
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: stockLocationsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: stockLocationsQueryKeys.detail(location.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
t("stockLocations.delete.successToast", {
|
||||||
|
name: location.name,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
toast.error((e as FetchError).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: t("fields.name"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const name = getValue()
|
||||||
|
if (!name) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="text-ui-fg-subtle text-small truncate">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("address", {
|
||||||
|
header: t("fields.address"),
|
||||||
|
cell: ({ getValue, row }) => {
|
||||||
|
const address = getValue()
|
||||||
|
const location = row.original
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-ui-fg-subtle text-small truncate">
|
||||||
|
{getFormattedAddress({
|
||||||
|
address: location.address as HttpTypes.AdminOrderAddress,
|
||||||
|
}).join(", ")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("fulfillment_sets", {
|
||||||
|
id: "shipping_fulfillment",
|
||||||
|
header: t("stockLocations.fulfillmentSets.shipping.header"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const fulfillmentSets = getValue()
|
||||||
|
const shippingSet = fulfillmentSets?.find(
|
||||||
|
(f) => f.type === FulfillmentSetType.Shipping
|
||||||
|
)
|
||||||
|
const fulfillmentSetExists = !!shippingSet
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusBadge color={fulfillmentSetExists ? "green" : "grey"}>
|
||||||
|
{t(
|
||||||
|
fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled"
|
||||||
|
)}
|
||||||
|
</StatusBadge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("fulfillment_sets", {
|
||||||
|
id: "pickup_fulfillment",
|
||||||
|
header: t("stockLocations.fulfillmentSets.pickup.header"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const fulfillmentSets = getValue()
|
||||||
|
const pickupSet = fulfillmentSets?.find(
|
||||||
|
(f) => f.type === FulfillmentSetType.Pickup
|
||||||
|
)
|
||||||
|
const fulfillmentSetExists = !!pickupSet
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusBadge color={fulfillmentSetExists ? "green" : "grey"}>
|
||||||
|
{t(
|
||||||
|
fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled"
|
||||||
|
)}
|
||||||
|
</StatusBadge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("sales_channels", {
|
||||||
|
header: t("stockLocations.salesChannels.label"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const salesChannels = getValue()
|
||||||
|
|
||||||
|
if (!salesChannels?.length) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<ListSummary
|
||||||
|
inline
|
||||||
|
n={1}
|
||||||
|
list={salesChannels.map((s) => s.name)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.action({
|
||||||
|
actions: (ctx) => {
|
||||||
|
const location = ctx.row.original
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
label: t("actions.edit"),
|
||||||
|
onClick: () => {
|
||||||
|
navigate(`/settings/locations/${location.id}/edit`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: () => handleDelete(location),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { useQueryParams } from "../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
export const useLocationListTableQuery = ({
|
||||||
|
pageSize = 20,
|
||||||
|
prefix,
|
||||||
|
}: {
|
||||||
|
pageSize?: number
|
||||||
|
prefix?: string
|
||||||
|
}) => {
|
||||||
|
const queryObject = useQueryParams(["order", "offset", "q"], prefix)
|
||||||
|
|
||||||
|
const { offset, ...rest } = queryObject
|
||||||
|
|
||||||
|
const searchParams: HttpTypes.AdminStockLocationListParams = {
|
||||||
|
limit: pageSize,
|
||||||
|
offset: offset ? Number(offset) : 0,
|
||||||
|
...rest,
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchParams
|
||||||
|
}
|
||||||
+12
-4
@@ -1,12 +1,20 @@
|
|||||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { Container, createDataTableColumnHelper, toast, usePrompt, } from "@medusajs/ui"
|
import {
|
||||||
|
Container,
|
||||||
|
createDataTableColumnHelper,
|
||||||
|
toast,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
import { keepPreviousData } from "@tanstack/react-query"
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
import { useCallback, useMemo } from "react"
|
import { useCallback, useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import { DataTable } from "../../../../../components/data-table"
|
import { DataTable } from "../../../../../components/data-table"
|
||||||
import { useDeleteRefundReasonLazy, useRefundReasons, } from "../../../../../hooks/api"
|
import {
|
||||||
|
useDeleteRefundReasonLazy,
|
||||||
|
useRefundReasons,
|
||||||
|
} from "../../../../../hooks/api"
|
||||||
import { useRefundReasonTableColumns } from "../../../../../hooks/table/columns"
|
import { useRefundReasonTableColumns } from "../../../../../hooks/table/columns"
|
||||||
import { useRefundReasonTableQuery } from "../../../../../hooks/table/query"
|
import { useRefundReasonTableQuery } from "../../../../../hooks/table/query"
|
||||||
|
|
||||||
@@ -18,7 +26,7 @@ export const RefundReasonListTable = () => {
|
|||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { refund_reasons, count, isPending, isError, error } = useRefundReasons(
|
const { refund_reasons, count, isLoading, isError, error } = useRefundReasons(
|
||||||
searchParams,
|
searchParams,
|
||||||
{
|
{
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
@@ -56,7 +64,7 @@ export const RefundReasonListTable = () => {
|
|||||||
to: "create",
|
to: "create",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
isLoading={isPending}
|
isLoading={isLoading}
|
||||||
enableSearch={true}
|
enableSearch={true}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { StockLocation } from "@models"
|
|||||||
const StockLocationAddress = model
|
const StockLocationAddress = model
|
||||||
.define("StockLocationAddress", {
|
.define("StockLocationAddress", {
|
||||||
id: model.id({ prefix: "laddr" }).primaryKey(),
|
id: model.id({ prefix: "laddr" }).primaryKey(),
|
||||||
address_1: model.text(),
|
address_1: model.text().searchable(),
|
||||||
address_2: model.text().nullable(),
|
address_2: model.text().searchable().nullable(),
|
||||||
company: model.text().nullable(),
|
company: model.text().nullable(),
|
||||||
city: model.text().nullable(),
|
city: model.text().searchable().nullable(),
|
||||||
country_code: model.text(),
|
country_code: model.text().searchable(),
|
||||||
phone: model.text().nullable(),
|
phone: model.text().nullable(),
|
||||||
province: model.text().nullable(),
|
province: model.text().searchable().nullable(),
|
||||||
postal_code: model.text().nullable(),
|
postal_code: model.text().searchable().nullable(),
|
||||||
metadata: model.json().nullable(),
|
metadata: model.json().nullable(),
|
||||||
stock_locations: model.hasOne(() => StockLocation, {
|
stock_locations: model.hasOne(() => StockLocation, {
|
||||||
mappedBy: "address",
|
mappedBy: "address",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const StockLocation = model.define("StockLocation", {
|
|||||||
.belongsTo(() => StockLocationAddress, {
|
.belongsTo(() => StockLocationAddress, {
|
||||||
mappedBy: "stock_locations",
|
mappedBy: "stock_locations",
|
||||||
})
|
})
|
||||||
|
.searchable()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user