feat(ui,dashboard): Migrate SC tables to DataTable (#11106)

This commit is contained in:
Kasper Fabricius Kristensen
2025-02-07 14:26:49 +00:00
committed by GitHub
parent d588073cea
commit fcd3e2226e
34 changed files with 847 additions and 858 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/ui": patch
"@medusajs/dashboard": patch
---
feat(ui,dashboard): Migrate SC tables to DataTable
+7 -7
View File
@@ -45,10 +45,10 @@
"@dnd-kit/sortable": "^8.0.0", "@dnd-kit/sortable": "^8.0.0",
"@hookform/error-message": "^2.0.1", "@hookform/error-message": "^2.0.1",
"@hookform/resolvers": "3.4.2", "@hookform/resolvers": "3.4.2",
"@medusajs/admin-shared": "2.4.0", "@medusajs/admin-shared": "~2.4.0",
"@medusajs/icons": "2.4.0", "@medusajs/icons": "~2.4.0",
"@medusajs/js-sdk": "2.4.0", "@medusajs/js-sdk": "~2.4.0",
"@medusajs/ui": "4.0.4", "@medusajs/ui": "~4.0.4",
"@tanstack/react-query": "5.64.2", "@tanstack/react-query": "5.64.2",
"@tanstack/react-table": "8.20.5", "@tanstack/react-table": "8.20.5",
"@tanstack/react-virtual": "^3.8.3", "@tanstack/react-virtual": "^3.8.3",
@@ -76,9 +76,9 @@
}, },
"devDependencies": { "devDependencies": {
"@medusajs/admin-shared": "~2.4.0", "@medusajs/admin-shared": "~2.4.0",
"@medusajs/admin-vite-plugin": "2.4.0", "@medusajs/admin-vite-plugin": "~2.4.0",
"@medusajs/types": "2.4.0", "@medusajs/types": "~2.4.0",
"@medusajs/ui-preset": "2.4.0", "@medusajs/ui-preset": "~2.4.0",
"@types/node": "^20.11.15", "@types/node": "^20.11.15",
"@types/react": "^18.2.79", "@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25", "@types/react-dom": "^18.2.25",
@@ -0,0 +1,35 @@
import { clx } from "@medusajs/ui"
import { PropsWithChildren } from "react"
type DataTableStatusCellProps = PropsWithChildren<{
color?: "green" | "red" | "blue" | "orange" | "grey" | "purple"
}>
export const DataTableStatusCell = ({
color,
children,
}: DataTableStatusCellProps) => {
return (
<div className="txt-compact-small text-ui-fg-subtle flex h-full w-full items-center gap-x-2 overflow-hidden">
<div
role="presentation"
className="flex h-5 w-2 items-center justify-center"
>
<div
className={clx(
"h-2 w-2 rounded-sm shadow-[0px_0px_0px_1px_rgba(0,0,0,0.12)_inset]",
{
"bg-ui-tag-neutral-icon": color === "grey",
"bg-ui-tag-green-icon": color === "green",
"bg-ui-tag-red-icon": color === "red",
"bg-ui-tag-blue-icon": color === "blue",
"bg-ui-tag-orange-icon": color === "orange",
"bg-ui-tag-purple-icon": color === "purple",
}
)}
/>
</div>
<span className="truncate">{children}</span>
</div>
)
}
@@ -1,15 +1,18 @@
import { import {
Button, Button,
clx,
DataTableColumnDef, DataTableColumnDef,
DataTableCommand, DataTableCommand,
DataTableEmptyStateProps, DataTableEmptyStateProps,
DataTableFilter, DataTableFilter,
DataTableFilteringState, DataTableFilteringState,
DataTablePaginationState, DataTablePaginationState,
DataTableRow,
DataTableRowSelectionState, DataTableRowSelectionState,
DataTableSortingState, DataTableSortingState,
Heading, Heading,
DataTable as Primitive, DataTable as Primitive,
Text,
useDataTable, useDataTable,
} from "@medusajs/ui" } from "@medusajs/ui"
import React, { ReactNode, useCallback, useState } from "react" import React, { ReactNode, useCallback, useState } from "react"
@@ -66,14 +69,17 @@ interface DataTableProps<TData> {
autoFocusSearch?: boolean autoFocusSearch?: boolean
rowHref?: (row: TData) => string rowHref?: (row: TData) => string
emptyState?: DataTableEmptyStateProps emptyState?: DataTableEmptyStateProps
heading: string heading?: string
subHeading?: string
prefix?: string prefix?: string
pageSize?: number pageSize?: number
isLoading?: boolean isLoading?: boolean
rowSelection?: { rowSelection?: {
state: DataTableRowSelectionState state: DataTableRowSelectionState
onRowSelectionChange: (value: DataTableRowSelectionState) => void onRowSelectionChange: (value: DataTableRowSelectionState) => void
enableRowSelection?: boolean | ((row: DataTableRow<TData>) => boolean)
} }
layout?: "fill" | "auto"
} }
export const DataTable = <TData,>({ export const DataTable = <TData,>({
@@ -90,11 +96,13 @@ export const DataTable = <TData,>({
autoFocusSearch = false, autoFocusSearch = false,
rowHref, rowHref,
heading, heading,
subHeading,
prefix, prefix,
pageSize = 10, pageSize = 10,
emptyState, emptyState,
rowSelection, rowSelection,
isLoading = false, isLoading = false,
layout = "auto",
}: DataTableProps<TData>) => { }: DataTableProps<TData>) => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -258,14 +266,30 @@ export const DataTable = <TData,>({
isLoading, isLoading,
}) })
const shouldRenderHeading = heading || subHeading
return ( return (
<Primitive instance={instance}> <Primitive
instance={instance}
className={clx({
"h-full [&_tr]:last-of-type:!border-b": layout === "fill",
})}
>
<Primitive.Toolbar <Primitive.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"
translations={toolbarTranslations} translations={toolbarTranslations}
> >
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between gap-2">
<Heading>{heading}</Heading> {shouldRenderHeading && (
<div>
{heading && <Heading>{heading}</Heading>}
{subHeading && (
<Text size="small" className="text-ui-fg-subtle">
{subHeading}
</Text>
)}
</div>
)}
<div className="flex items-center justify-end gap-x-2 md:hidden"> <div className="flex items-center justify-end gap-x-2 md:hidden">
{enableFiltering && ( {enableFiltering && (
<Primitive.FilterMenu tooltip={t("filters.filterLabel")} /> <Primitive.FilterMenu tooltip={t("filters.filterLabel")} />
@@ -0,0 +1,61 @@
import {
createDataTableColumnHelper,
DataTableColumnDef,
Tooltip,
} from "@medusajs/ui"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useDate } from "../../../../hooks/use-date"
type EntityWithDates = {
created_at: string
updated_at: string
}
const columnHelper = createDataTableColumnHelper<EntityWithDates>()
export const useDataTableDateColumns = <TData extends EntityWithDates>() => {
const { t } = useTranslation()
const { getFullDate } = useDate()
return useMemo(() => {
return [
columnHelper.accessor("created_at", {
header: t("fields.createdAt"),
cell: ({ row }) => {
return (
<Tooltip
content={getFullDate({
date: row.original.created_at,
includeTime: true,
})}
>
<span>{getFullDate({ date: row.original.created_at })}</span>
</Tooltip>
)
},
enableSorting: true,
sortAscLabel: t("filters.sorting.dateAsc"),
sortDescLabel: t("filters.sorting.dateDesc"),
}),
columnHelper.accessor("updated_at", {
header: t("fields.updatedAt"),
cell: ({ row }) => {
return (
<Tooltip
content={getFullDate({
date: row.original.updated_at,
includeTime: true,
})}
>
<span>{getFullDate({ date: row.original.updated_at })}</span>
</Tooltip>
)
},
enableSorting: true,
sortAscLabel: t("filters.sorting.dateAsc"),
sortDescLabel: t("filters.sorting.dateDesc"),
}),
] as DataTableColumnDef<TData>[]
}, [t, getFullDate])
}
@@ -0,0 +1,4 @@
export * from "./use-sales-channel-table-columns"
export * from "./use-sales-channel-table-empty-state"
export * from "./use-sales-channel-table-filters"
export * from "./use-sales-channel-table-query"
@@ -0,0 +1,61 @@
import { HttpTypes } from "@medusajs/types"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { createDataTableColumnHelper, Tooltip } from "@medusajs/ui"
import { DataTableStatusCell } from "../../components/data-table-status-cell/data-table-status-cell"
import { useDataTableDateColumns } from "../general/use-data-table-date-columns"
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminSalesChannel>()
export const useSalesChannelTableColumns = () => {
const { t } = useTranslation()
const dateColumns = useDataTableDateColumns<HttpTypes.AdminSalesChannel>()
return useMemo(
() => [
columnHelper.accessor("name", {
header: () => t("fields.name"),
enableSorting: true,
sortLabel: t("fields.name"),
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
}),
columnHelper.accessor("description", {
header: () => t("fields.description"),
cell: ({ getValue }) => {
return (
<Tooltip content={getValue()}>
<div className="flex h-full w-full items-center overflow-hidden">
<span className="truncate">{getValue()}</span>
</div>
</Tooltip>
)
},
enableSorting: true,
sortLabel: t("fields.description"),
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
maxSize: 250,
minSize: 100,
}),
columnHelper.accessor("is_disabled", {
header: () => t("fields.status"),
enableSorting: true,
sortLabel: t("fields.status"),
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
cell: ({ getValue }) => {
const value = getValue()
return (
<DataTableStatusCell color={value ? "grey" : "green"}>
{value ? t("general.disabled") : t("general.enabled")}
</DataTableStatusCell>
)
},
}),
...dateColumns,
],
[t, dateColumns]
)
}
@@ -0,0 +1,22 @@
import { DataTableEmptyStateProps } from "@medusajs/ui"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
export const useSalesChannelTableEmptyState = (): DataTableEmptyStateProps => {
const { t } = useTranslation()
return useMemo(() => {
const content: DataTableEmptyStateProps = {
empty: {
heading: t("salesChannels.list.empty.heading"),
description: t("salesChannels.list.empty.description"),
},
filtered: {
heading: t("salesChannels.list.filtered.heading"),
description: t("salesChannels.list.filtered.description"),
},
}
return content
}, [t])
}
@@ -0,0 +1,33 @@
import { HttpTypes } from "@medusajs/types"
import { createDataTableFilterHelper } from "@medusajs/ui"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useDataTableDateFilters } from "../general/use-data-table-date-filters"
const filterHelper = createDataTableFilterHelper<HttpTypes.AdminSalesChannel>()
export const useSalesChannelTableFilters = () => {
const { t } = useTranslation()
const dateFilters = useDataTableDateFilters()
return useMemo(
() => [
filterHelper.accessor("is_disabled", {
label: t("fields.status"),
type: "radio",
options: [
{
label: t("general.enabled"),
value: "false",
},
{
label: t("general.disabled"),
value: "true",
},
],
}),
...dateFilters,
],
[dateFilters, t]
)
}
@@ -1,5 +1,5 @@
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { useQueryParams } from "../../use-query-params" import { useQueryParams } from "../../../../hooks/use-query-params"
type UseSalesChannelTableQueryProps = { type UseSalesChannelTableQueryProps = {
prefix?: string prefix?: string
@@ -22,17 +22,9 @@ export const useSalesChannelTableQuery = ({
offset: offset ? Number(offset) : 0, offset: offset ? Number(offset) : 0,
created_at: created_at ? JSON.parse(created_at) : undefined, created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined, updated_at: updated_at ? JSON.parse(updated_at) : undefined,
is_disabled: is_disabled: is_disabled ? JSON.parse(is_disabled) : undefined,
is_disabled === "true"
? true
: is_disabled === "false"
? false
: undefined,
...rest, ...rest,
} }
return { return searchParams
searchParams,
raw: queryObject,
}
} }
@@ -5,6 +5,9 @@ type StatusCellProps = PropsWithChildren<{
color?: "green" | "red" | "blue" | "orange" | "grey" | "purple" color?: "green" | "red" | "blue" | "orange" | "grey" | "purple"
}> }>
/**
* @deprecated Use the new DataTable and DataTableStatusCell instead
*/
export const StatusCell = ({ color, children }: StatusCellProps) => { export const StatusCell = ({ color, children }: StatusCellProps) => {
return ( return (
<div className="txt-compact-small text-ui-fg-subtle flex h-full w-full items-center gap-x-2 overflow-hidden"> <div className="txt-compact-small text-ui-fg-subtle flex h-full w-full items-center gap-x-2 overflow-hidden">
@@ -41,7 +41,7 @@ export const useSalesChannel = (
} }
export const useSalesChannels = ( export const useSalesChannels = (
query?: Record<string, any>, query?: HttpTypes.AdminSalesChannelListParams,
options?: Omit< options?: Omit<
UseQueryOptions< UseQueryOptions<
AdminSalesChannelListResponse, AdminSalesChannelListResponse,
@@ -133,6 +133,34 @@ export const useDeleteSalesChannel = (
}) })
} }
export const useDeleteSalesChannelLazy = (
options?: UseMutationOptions<
HttpTypes.AdminSalesChannelDeleteResponse,
FetchError,
string
>
) => {
return useMutation({
mutationFn: (id: string) => sdk.admin.salesChannel.delete(id),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: salesChannelsQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: salesChannelsQueryKeys.detail(variables),
})
// Invalidate all products to ensure they are updated if they were linked to the sales channel
queryClient.invalidateQueries({
queryKey: productsQueryKeys.all,
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useSalesChannelRemoveProducts = ( export const useSalesChannelRemoveProducts = (
id: string, id: string,
options?: UseMutationOptions< options?: UseMutationOptions<
@@ -8,6 +8,4 @@ export * from "./use-product-tag-table-columns"
export * from "./use-product-type-table-columns" export * from "./use-product-type-table-columns"
export * from "./use-region-table-columns" export * from "./use-region-table-columns"
export * from "./use-return-reason-table-columns" export * from "./use-return-reason-table-columns"
export * from "./use-sales-channel-table-columns"
export * from "./use-tax-rates-table-columns" export * from "./use-tax-rates-table-columns"
@@ -1,47 +0,0 @@
import { createColumnHelper } from "@tanstack/react-table"
import { HttpTypes } from "@medusajs/types"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { StatusCell } from "../../../components/table/table-cells/common/status-cell"
import { TextHeader } from "../../../components/table/table-cells/common/text-cell"
import {
DescriptionCell,
DescriptionHeader,
} from "../../../components/table/table-cells/sales-channel/description-cell"
import {
NameCell,
NameHeader,
} from "../../../components/table/table-cells/sales-channel/name-cell"
const columnHelper = createColumnHelper<HttpTypes.AdminSalesChannel>()
export const useSalesChannelTableColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.accessor("name", {
header: () => <NameHeader />,
cell: ({ getValue }) => <NameCell name={getValue()} />,
}),
columnHelper.accessor("description", {
header: () => <DescriptionHeader />,
cell: ({ getValue }) => <DescriptionCell description={getValue()} />,
}),
columnHelper.accessor("is_disabled", {
header: () => <TextHeader text={t("fields.status")} />,
cell: ({ getValue }) => {
const value = getValue()
return (
<StatusCell color={value ? "grey" : "green"}>
{value ? t("general.disabled") : t("general.enabled")}
</StatusCell>
)
},
}),
],
[t]
)
}
@@ -8,6 +8,5 @@ export * from "./use-product-tag-table-filters"
export * from "./use-product-type-table-filters" export * from "./use-product-type-table-filters"
export * from "./use-promotion-table-filters" export * from "./use-promotion-table-filters"
export * from "./use-region-table-filters" export * from "./use-region-table-filters"
export * from "./use-sales-channel-table-filters"
export * from "./use-shipping-option-table-filters" export * from "./use-shipping-option-table-filters"
export * from "./use-tax-rate-table-filters" export * from "./use-tax-rate-table-filters"
@@ -1,17 +0,0 @@
import { useTranslation } from "react-i18next"
import { Filter } from "../../../components/table/data-table"
export const useSalesChannelTableFilters = () => {
const { t } = useTranslation()
const dateFilters: Filter[] = [
{ label: t("fields.createdAt"), key: "created_at" },
{ label: t("fields.updatedAt"), key: "updated_at" },
].map((f) => ({
key: f.key,
label: f.label,
type: "date",
}))
return dateFilters
}
@@ -8,7 +8,6 @@ export * from "./use-product-tag-table-query"
export * from "./use-product-type-table-query" export * from "./use-product-type-table-query"
export * from "./use-region-table-query" export * from "./use-region-table-query"
export * from "./use-return-reason-table-query" export * from "./use-return-reason-table-query"
export * from "./use-sales-channel-table-query"
export * from "./use-shipping-option-table-query" export * from "./use-shipping-option-table-query"
export * from "./use-tax-rate-table-query" export * from "./use-tax-rate-table-query"
export * from "./use-tax-region-table-query" export * from "./use-tax-region-table-query"
@@ -5724,6 +5724,9 @@
"header": { "header": {
"type": "string" "type": "string"
}, },
"hint": {
"type": "string"
},
"label": { "label": {
"type": "string" "type": "string"
}, },
@@ -5742,6 +5745,7 @@
}, },
"required": [ "required": [
"header", "header",
"hint",
"label", "label",
"connectedTo", "connectedTo",
"noChannels", "noChannels",
@@ -9202,6 +9206,39 @@
"subtitle": { "subtitle": {
"type": "string" "type": "string"
}, },
"list": {
"type": "object",
"properties": {
"empty": {
"type": "object",
"properties": {
"heading": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["heading", "description"],
"additionalProperties": false
},
"filtered": {
"type": "object",
"properties": {
"heading": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": ["heading", "description"],
"additionalProperties": false
}
},
"required": ["empty", "filtered"],
"additionalProperties": false
},
"createSalesChannel": { "createSalesChannel": {
"type": "string" "type": "string"
}, },
@@ -9293,6 +9330,7 @@
"required": [ "required": [
"domain", "domain",
"subtitle", "subtitle",
"list",
"createSalesChannel", "createSalesChannel",
"createSalesChannelHint", "createSalesChannelHint",
"enabledHint", "enabledHint",
@@ -1513,6 +1513,7 @@
}, },
"salesChannels": { "salesChannels": {
"header": "Sales Channels", "header": "Sales Channels",
"hint": "Manage the sales channels that are connected to this location.",
"label": "Connected sales channels", "label": "Connected sales channels",
"connectedTo": "Connected to {{count}} of {{total}} sales channels", "connectedTo": "Connected to {{count}} of {{total}} sales channels",
"noChannels": "The location is not connected to any sales channels.", "noChannels": "The location is not connected to any sales channels.",
@@ -2469,6 +2470,16 @@
"salesChannels": { "salesChannels": {
"domain": "Sales Channels", "domain": "Sales Channels",
"subtitle": "Manage the online and offline channels you sell products on.", "subtitle": "Manage the online and offline channels you sell products on.",
"list": {
"empty": {
"heading": "No sales channels found",
"description": "Once a sales channel has been created, it will appear here."
},
"filtered": {
"heading": "No results",
"description": "No sales channels match the current filter criteria."
}
},
"createSalesChannel": "Create Sales Channel", "createSalesChannel": "Create Sales Channel",
"createSalesChannelHint": "Create a new sales channel to sell your products on.", "createSalesChannelHint": "Create a new sales channel to sell your products on.",
"enabledHint": "Specify whether the sales channel is enabled.", "enabledHint": "Specify whether the sales channel is enabled.",
@@ -1,270 +1,205 @@
import { PencilSquare, Plus, Trash } from "@medusajs/icons" import { PencilSquare, Trash } from "@medusajs/icons"
import { AdminApiKeyResponse, AdminSalesChannelResponse } from "@medusajs/types" import { AdminApiKeyResponse, HttpTypes } from "@medusajs/types"
import { Checkbox, Container, Heading, toast, usePrompt } from "@medusajs/ui" import {
Container,
createDataTableColumnHelper,
createDataTableCommandHelper,
DataTableRowSelectionState,
toast,
usePrompt,
} from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query" import { keepPreviousData } from "@tanstack/react-query"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table" import { RowSelectionState } from "@tanstack/react-table"
import { useMemo, useState } from "react" import { useCallback, useMemo, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu" import { useNavigate } from "react-router-dom"
import { _DataTable } from "../../../../../components/table/data-table" import { DataTable } from "../../../../../components/data-table"
import * as hooks from "../../../../../components/data-table/helpers/sales-channels"
import { useBatchRemoveSalesChannelsFromApiKey } from "../../../../../hooks/api/api-keys" import { useBatchRemoveSalesChannelsFromApiKey } from "../../../../../hooks/api/api-keys"
import { useSalesChannels } from "../../../../../hooks/api/sales-channels" import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters"
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
type ApiKeySalesChannelSectionProps = { type ApiKeySalesChannelSectionProps = {
apiKey: AdminApiKeyResponse["api_key"] apiKey: AdminApiKeyResponse["api_key"]
} }
const PAGE_SIZE = 10 const PAGE_SIZE = 10
const PREFIX = "sc"
export const ApiKeySalesChannelSection = ({ export const ApiKeySalesChannelSection = ({
apiKey, apiKey,
}: ApiKeySalesChannelSectionProps) => { }: ApiKeySalesChannelSectionProps) => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const { t } = useTranslation() const { t } = useTranslation()
const prompt = usePrompt()
const { raw, searchParams } = useSalesChannelTableQuery({ const searchParams = hooks.useSalesChannelTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
prefix: PREFIX,
}) })
const { sales_channels, count, isLoading } = useSalesChannels( const { sales_channels, count, isPending } = useSalesChannels(
{ ...searchParams, publishable_key_id: apiKey.id }, { ...searchParams, publishable_key_id: apiKey.id },
{ {
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
} }
) )
const columns = useColumns() const columns = useColumns(apiKey.id)
const filters = useSalesChannelTableFilters() const filters = hooks.useSalesChannelTableFilters()
const commands = useCommands(apiKey.id, setRowSelection)
const { table } = useDataTable({ const emptyState = hooks.useSalesChannelTableEmptyState()
data: sales_channels ?? [],
columns,
count,
enablePagination: true,
enableRowSelection: true,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
rowSelection: {
state: rowSelection,
updater: setRowSelection,
},
meta: {
apiKey: apiKey.id,
},
})
const { mutateAsync } = useBatchRemoveSalesChannelsFromApiKey(apiKey.id)
const handleRemove = async () => {
const keys = Object.keys(rowSelection)
const res = await prompt({
title: t("general.areYouSure"),
description: t("apiKeyManagement.removeSalesChannel.warningBatch", {
count: keys.length,
}),
confirmText: t("actions.continue"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync(keys, {
onSuccess: () => {
toast.success(
t("apiKeyManagement.removeSalesChannel.successToastBatch", {
count: keys.length,
})
)
setRowSelection({})
},
onError: (err) => {
toast.error(err.message)
},
})
}
return ( return (
<Container className="divide-y p-0"> <Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4"> <DataTable
<Heading level="h2">{t("salesChannels.domain")}</Heading> data={sales_channels}
<ActionMenu
groups={[
{
actions: [
{
icon: <Plus />,
label: t("actions.add"),
to: "sales-channels",
},
],
},
]}
/>
</div>
<_DataTable
table={table}
columns={columns} columns={columns}
filters={filters} filters={filters}
count={count} commands={commands}
isLoading={isLoading} heading={t("salesChannels.domain")}
queryObject={raw} getRowId={(row) => row.id}
navigateTo={(row) => `/settings/sales-channels/${row.id}`} rowCount={count}
orderBy={[ isLoading={isPending}
{ emptyState={emptyState}
key: "name", rowSelection={{
label: t("fields.name"), state: rowSelection,
}, onRowSelectionChange: setRowSelection,
{
key: "created_at",
label: t("fields.createdAt"),
},
{
key: "updated_at",
label: t("fields.updatedAt"),
},
]}
commands={[
{
action: handleRemove,
label: t("actions.remove"),
shortcut: "r",
},
]}
pageSize={PAGE_SIZE}
pagination
search
noRecords={{
message: t("apiKeyManagement.salesChannels.list.noRecordsMessage"),
}} }}
rowHref={(row) => `/settings/sales-channels/${row.id}`}
action={{
label: t("actions.add"),
to: "sales-channels",
}}
prefix={PREFIX}
pageSize={PAGE_SIZE}
/> />
</Container> </Container>
) )
} }
const SalesChannelActions = ({ const columnHelper = createDataTableColumnHelper<HttpTypes.AdminSalesChannel>()
salesChannel,
apiKey, const useColumns = (id: string) => {
}: {
salesChannel: AdminSalesChannelResponse["sales_channel"]
apiKey: string
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate()
const prompt = usePrompt() const prompt = usePrompt()
const { mutateAsync } = useBatchRemoveSalesChannelsFromApiKey(apiKey) const base = hooks.useSalesChannelTableColumns()
const handleDelete = async () => { const { mutateAsync } = useBatchRemoveSalesChannelsFromApiKey(id)
const res = await prompt({
title: t("general.areYouSure"),
description: t("apiKeyManagement.removeSalesChannel.warning", {
name: salesChannel.name,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) { const handleDelete = useCallback(
return async (salesChannel: HttpTypes.AdminSalesChannel) => {
} const res = await prompt({
title: t("general.areYouSure"),
description: t("apiKeyManagement.removeSalesChannel.warning", {
name: salesChannel.name,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
await mutateAsync([salesChannel.id], { if (!res) {
onSuccess: () => { return
toast.success( }
t("apiKeyManagement.removeSalesChannel.successToast", {
count: 1,
})
)
},
onError: (err) => {
toast.error(err.message)
},
})
}
return ( await mutateAsync([salesChannel.id], {
<ActionMenu onSuccess: () => {
groups={[ toast.success(
{ t("apiKeyManagement.removeSalesChannel.successToast", {
actions: [ count: 1,
{ })
icon: <PencilSquare />, )
label: t("actions.edit"),
to: `/settings/sales-channels/${salesChannel.id}/edit`,
},
],
}, },
{ onError: (err) => {
actions: [ toast.error(err.message)
{
icon: <Trash />,
label: t("actions.delete"),
onClick: handleDelete,
},
],
}, },
]} })
/> },
[mutateAsync, prompt, t]
) )
}
const columnHelper =
createColumnHelper<AdminSalesChannelResponse["sales_channel"]>()
const useColumns = () => {
const base = useSalesChannelTableColumns()
return useMemo( return useMemo(
() => [ () => [
columnHelper.display({ columnHelper.select(),
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()
}}
/>
)
},
}),
...base, ...base,
columnHelper.display({ columnHelper.action({
id: "actions", actions: (ctx) => [
cell: ({ row, table }) => { [
const { apiKey } = table.options.meta as { {
apiKey: string label: t("actions.edit"),
} icon: <PencilSquare />,
onClick: () => {
return ( navigate(`/settings/sales-channels/${ctx.row.original.id}/edit`)
<SalesChannelActions salesChannel={row.original} apiKey={apiKey} /> },
) },
}, ],
[
{
icon: <Trash />,
label: t("actions.delete"),
onClick: () => handleDelete(ctx.row.original),
},
],
],
}), }),
], ],
[base] [base, handleDelete, navigate, t]
)
}
const commandHelper = createDataTableCommandHelper()
const useCommands = (
id: string,
setRowSelection: (state: DataTableRowSelectionState) => void
) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useBatchRemoveSalesChannelsFromApiKey(id)
const handleRemove = useCallback(
async (rowSelection: DataTableRowSelectionState) => {
const keys = Object.keys(rowSelection)
const res = await prompt({
title: t("general.areYouSure"),
description: t("apiKeyManagement.removeSalesChannel.warningBatch", {
count: keys.length,
}),
confirmText: t("actions.continue"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync(keys, {
onSuccess: () => {
toast.success(
t("apiKeyManagement.removeSalesChannel.successToastBatch", {
count: keys.length,
})
)
setRowSelection({})
},
onError: (err) => {
toast.error(err.message)
},
})
},
[mutateAsync, prompt, t, setRowSelection]
)
return useMemo(
() => [
commandHelper.command({
action: handleRemove,
label: t("actions.remove"),
shortcut: "r",
}),
],
[handleRemove, t]
) )
} }
@@ -1,29 +1,31 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { AdminSalesChannelResponse } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { Button, Checkbox, Hint, Tooltip, toast } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { import {
OnChangeFn, Button,
RowSelectionState, Checkbox,
createColumnHelper, DataTableRowSelectionState,
} from "@tanstack/react-table" Hint,
createDataTableColumnHelper,
toast,
} from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { RowSelectionState } from "@tanstack/react-table"
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { ConditionalTooltip } from "../../../../../components/common/conditional-tooltip"
import { DataTable } from "../../../../../components/data-table"
import * as hooks from "../../../../../components/data-table/helpers/sales-channels"
import { import {
RouteFocusModal, RouteFocusModal,
useRouteModal, useRouteModal,
} from "../../../../../components/modals" } from "../../../../../components/modals"
import { _DataTable } from "../../../../../components/table/data-table"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form" import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { VisuallyHidden } from "../../../../../components/utilities/visually-hidden" import { VisuallyHidden } from "../../../../../components/utilities/visually-hidden"
import { useBatchAddSalesChannelsToApiKey } from "../../../../../hooks/api/api-keys" import { useBatchAddSalesChannelsToApiKey } from "../../../../../hooks/api/api-keys"
import { useSalesChannels } from "../../../../../hooks/api/sales-channels" import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters"
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
type ApiKeySalesChannelFormProps = { type ApiKeySalesChannelFormProps = {
apiKey: string apiKey: string
@@ -31,10 +33,11 @@ type ApiKeySalesChannelFormProps = {
} }
const AddSalesChannelsToApiKeySchema = zod.object({ const AddSalesChannelsToApiKeySchema = zod.object({
sales_channel_ids: zod.array(zod.string()), sales_channel_ids: zod.array(zod.string()).min(1),
}) })
const PAGE_SIZE = 50 const PAGE_SIZE = 50
const PREFIX = "sc_add"
export const ApiKeySalesChannelsForm = ({ export const ApiKeySalesChannelsForm = ({
apiKey, apiKey,
@@ -54,51 +57,36 @@ export const ApiKeySalesChannelsForm = ({
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const { mutateAsync, isPending } = useBatchAddSalesChannelsToApiKey(apiKey) const { mutateAsync, isPending: isMutating } =
useBatchAddSalesChannelsToApiKey(apiKey)
const { raw, searchParams } = useSalesChannelTableQuery({ const searchParams = hooks.useSalesChannelTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
prefix: PREFIX,
}) })
const columns = useColumns() const columns = useColumns()
const filters = useSalesChannelTableFilters() const filters = hooks.useSalesChannelTableFilters()
const emptyState = hooks.useSalesChannelTableEmptyState()
const { sales_channels, count, isLoading } = useSalesChannels( const { sales_channels, count, isPending } = useSalesChannels(
{ ...searchParams }, { ...searchParams },
{ {
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
} }
) )
const updater: OnChangeFn<RowSelectionState> = (fn) => { const updater = (selection: DataTableRowSelectionState) => {
const state = typeof fn === "function" ? fn(rowSelection) : fn const ids = Object.keys(selection)
const ids = Object.keys(state)
setValue("sales_channel_ids", ids, { setValue("sales_channel_ids", ids, {
shouldDirty: true, shouldDirty: true,
shouldTouch: true, shouldTouch: true,
}) })
setRowSelection(state) setRowSelection(selection)
} }
const { table } = useDataTable({
data: sales_channels ?? [],
columns,
count,
enablePagination: true,
enableRowSelection: (row) => {
return !preSelected.includes(row.original.id!)
},
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
rowSelection: {
state: rowSelection,
updater,
},
})
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(values.sales_channel_ids, { await mutateAsync(values.sales_channel_ids, {
onSuccess: () => { onSuccess: () => {
@@ -139,27 +127,23 @@ export const ApiKeySalesChannelsForm = ({
</div> </div>
</RouteFocusModal.Header> </RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-auto"> <RouteFocusModal.Body className="flex flex-1 flex-col overflow-auto">
<_DataTable <DataTable
table={table} data={sales_channels}
columns={columns} columns={columns}
count={count}
pageSize={PAGE_SIZE}
filters={filters} filters={filters}
pagination getRowId={(row) => row.id}
search="autofocus" rowCount={count}
isLoading={isLoading}
queryObject={raw}
orderBy={[
{ key: "name", label: t("fields.name") },
{ key: "created_at", label: t("fields.createdAt") },
{ key: "updated_at", label: t("fields.updatedAt") },
]}
layout="fill" layout="fill"
noRecords={{ emptyState={emptyState}
message: t( isLoading={isPending}
"apiKeyManagement.addSalesChannels.list.noRecordsMessage" rowSelection={{
), state: rowSelection,
onRowSelectionChange: updater,
enableRowSelection: (row) => !preSelected.includes(row.id),
}} }}
prefix={PREFIX}
pageSize={PAGE_SIZE}
autoFocusSearch
/> />
</RouteFocusModal.Body> </RouteFocusModal.Body>
<RouteFocusModal.Footer> <RouteFocusModal.Footer>
@@ -169,7 +153,7 @@ export const ApiKeySalesChannelsForm = ({
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</RouteFocusModal.Close> </RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isPending}> <Button size="small" type="submit" isLoading={isMutating}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
@@ -179,60 +163,36 @@ export const ApiKeySalesChannelsForm = ({
) )
} }
const columnHelper = const columnHelper = createDataTableColumnHelper<HttpTypes.AdminSalesChannel>()
createColumnHelper<AdminSalesChannelResponse["sales_channel"]>()
const useColumns = () => { const useColumns = () => {
const { t } = useTranslation() const { t } = useTranslation()
const base = useSalesChannelTableColumns() const base = hooks.useSalesChannelTableColumns()
return useMemo( return useMemo(
() => [ () => [
columnHelper.display({ columnHelper.select({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
cell: ({ row }) => { cell: ({ row }) => {
const isPreSelected = !row.getCanSelect() const isPreSelected = !row.getCanSelect()
const isSelected = row.getIsSelected() || isPreSelected const isSelected = row.getIsSelected() || isPreSelected
const Component = ( return (
<Checkbox <ConditionalTooltip
checked={isSelected} content={t("apiKeyManagement.salesChannels.alreadyAddedTooltip")}
disabled={isPreSelected} showTooltip={isPreSelected}
onCheckedChange={(value) => row.toggleSelected(!!value)} >
onClick={(e) => { <div>
e.stopPropagation() <Checkbox
}} checked={isSelected}
/> disabled={isPreSelected}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
</div>
</ConditionalTooltip>
) )
if (isPreSelected) {
return (
<Tooltip
content={t(
"apiKeyManagement.salesChannels.alreadyAddedTooltip"
)}
side="right"
>
{Component}
</Tooltip>
)
}
return Component
}, },
}), }),
...base, ...base,
@@ -1,10 +1,10 @@
import { PencilSquare, Trash } from "@medusajs/icons" import { PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { import {
Container, Container,
createDataTableColumnHelper, createDataTableColumnHelper,
toast, toast,
usePrompt, usePrompt,
} from "@medusajs/ui" } 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"
@@ -12,12 +12,12 @@ 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 { useDataTableDateFilters } from "../../../../../components/data-table/hooks/general/use-data-table-date-filters" import { useDataTableDateFilters } from "../../../../../components/data-table/helpers/general/use-data-table-date-filters"
import { SingleColumnPage } from "../../../../../components/layout/pages" import { SingleColumnPage } from "../../../../../components/layout/pages"
import { useDashboardExtension } from "../../../../../extensions" import { useDashboardExtension } from "../../../../../extensions"
import { import {
useCustomerGroups, useCustomerGroups,
useDeleteCustomerGroupLazy, useDeleteCustomerGroupLazy,
} from "../../../../../hooks/api" } from "../../../../../hooks/api"
import { useDate } from "../../../../../hooks/use-date" import { useDate } from "../../../../../hooks/use-date"
import { useQueryParams } from "../../../../../hooks/use-query-params" import { useQueryParams } from "../../../../../hooks/use-query-params"
@@ -1,24 +1,27 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { Button, Checkbox, toast } from "@medusajs/ui" import {
Button,
createDataTableColumnHelper,
DataTableRowSelectionState,
toast,
} from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query" import { keepPreviousData } from "@tanstack/react-query"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table" import { useMemo, useState } from "react"
import { useEffect, useMemo, useState } from "react" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
import { zodResolver } from "@hookform/resolvers/zod" import { DataTable } from "../../../../../components/data-table"
import { useForm } from "react-hook-form" import * as hooks from "../../../../../components/data-table/helpers/sales-channels"
import { import {
RouteFocusModal, RouteFocusModal,
useRouteModal, useRouteModal,
} from "../../../../../components/modals" } from "../../../../../components/modals"
import { _DataTable } from "../../../../../components/table/data-table" import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { VisuallyHidden } from "../../../../../components/utilities/visually-hidden"
import { useSalesChannels } from "../../../../../hooks/api/sales-channels" import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
import { useUpdateStockLocationSalesChannels } from "../../../../../hooks/api/stock-locations" import { useUpdateStockLocationSalesChannels } from "../../../../../hooks/api/stock-locations"
import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters"
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
type EditSalesChannelsFormProps = { type EditSalesChannelsFormProps = {
location: HttpTypes.AdminStockLocation location: HttpTypes.AdminStockLocation
@@ -29,6 +32,7 @@ const EditSalesChannelsSchema = zod.object({
}) })
const PAGE_SIZE = 50 const PAGE_SIZE = 50
const PREFIX = "sc"
export const LocationEditSalesChannelsForm = ({ export const LocationEditSalesChannelsForm = ({
location, location,
@@ -45,28 +49,25 @@ export const LocationEditSalesChannelsForm = ({
const { setValue } = form const { setValue } = form
const initialState = const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>(
location.sales_channels?.reduce((acc, curr) => { getInitialState(location)
acc[curr.id] = true )
return acc
}, {} as RowSelectionState) ?? {}
const [rowSelection, setRowSelection] = const onRowSelectionChange = (selection: DataTableRowSelectionState) => {
useState<RowSelectionState>(initialState) const ids = Object.keys(selection)
useEffect(() => {
const ids = Object.keys(rowSelection)
setValue("sales_channels", ids, { setValue("sales_channels", ids, {
shouldDirty: true, shouldDirty: true,
shouldTouch: true, shouldTouch: true,
}) })
}, [rowSelection, setValue]) setRowSelection(selection)
}
const { searchParams, raw } = useSalesChannelTableQuery({ const searchParams = hooks.useSalesChannelTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
prefix: PREFIX,
}) })
const { sales_channels, count, isLoading, isError, error } = useSalesChannels( const { sales_channels, count, isPending, isError, error } = useSalesChannels(
{ {
...searchParams, ...searchParams,
}, },
@@ -75,22 +76,9 @@ export const LocationEditSalesChannelsForm = ({
} }
) )
const filters = useSalesChannelTableFilters() const filters = hooks.useSalesChannelTableFilters()
const columns = useColumns() const columns = useColumns()
const emptyState = hooks.useSalesChannelTableEmptyState()
const { table } = useDataTable({
data: sales_channels ?? [],
columns,
count,
enablePagination: true,
enableRowSelection: true,
rowSelection: {
state: rowSelection,
updater: setRowSelection,
},
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
const { mutateAsync, isPending: isMutating } = const { mutateAsync, isPending: isMutating } =
useUpdateStockLocationSalesChannels(location.id) useUpdateStockLocationSalesChannels(location.id)
@@ -123,80 +111,67 @@ export const LocationEditSalesChannelsForm = ({
return ( return (
<RouteFocusModal.Form form={form}> <RouteFocusModal.Form form={form}>
<div className="flex h-full flex-col overflow-hidden"> <KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col">
<RouteFocusModal.Header> <RouteFocusModal.Header>
<RouteFocusModal.Title asChild>
<VisuallyHidden>
{t("stockLocations.salesChannels.header")}
</VisuallyHidden>
</RouteFocusModal.Title>
<RouteFocusModal.Description asChild>
<VisuallyHidden>
{t("stockLocations.salesChannels.hint")}
</VisuallyHidden>
</RouteFocusModal.Description>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-auto">
<DataTable
data={sales_channels}
columns={columns}
filters={filters}
emptyState={emptyState}
prefix={PREFIX}
rowSelection={{
state: rowSelection,
onRowSelectionChange,
}}
pageSize={PAGE_SIZE}
isLoading={isPending}
rowCount={count}
layout="fill"
getRowId={(row) => row.id}
/>
</RouteFocusModal.Body>
<RouteFocusModal.Footer>
<div className="flex items-center justify-end gap-x-2"> <div className="flex items-center justify-end gap-x-2">
<RouteFocusModal.Close asChild> <RouteFocusModal.Close asChild>
<Button size="small" variant="secondary"> <Button size="small" variant="secondary" type="button">
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</RouteFocusModal.Close> </RouteFocusModal.Close>
<Button size="small" isLoading={isMutating} onClick={handleSubmit}> <Button size="small" isLoading={isMutating} type="submit">
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
</RouteFocusModal.Header> </RouteFocusModal.Footer>
<RouteFocusModal.Body> </KeyboundForm>
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
isLoading={isLoading}
count={count}
filters={filters}
search="autofocus"
pagination
orderBy={[
{ key: "name", label: t("fields.name") },
{ key: "created_at", label: t("fields.createdAt") },
{ key: "updated_at", label: t("fields.updatedAt") },
]}
queryObject={raw}
layout="fill"
/>
</RouteFocusModal.Body>
</div>
</RouteFocusModal.Form> </RouteFocusModal.Form>
) )
} }
const columnHelper = createColumnHelper<HttpTypes.AdminSalesChannel>() const columnHelper = createDataTableColumnHelper<HttpTypes.AdminSalesChannel>()
const useColumns = () => { const useColumns = () => {
const columns = useSalesChannelTableColumns() const base = hooks.useSalesChannelTableColumns()
return useMemo( return useMemo(() => [columnHelper.select(), ...base], [base])
() => [ }
columnHelper.display({
id: "select", function getInitialState(location: HttpTypes.AdminStockLocation) {
header: ({ table }) => { return (
return ( location.sales_channels?.reduce((acc, curr) => {
<Checkbox acc[curr.id] = true
checked={ return acc
table.getIsSomePageRowsSelected() }, {} as DataTableRowSelectionState) ?? {}
? "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,24 +1,21 @@
import { AdminSalesChannelResponse } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { Button, Checkbox } from "@medusajs/ui"
import { import {
OnChangeFn, Button,
RowSelectionState, createDataTableColumnHelper,
createColumnHelper, DataTableRowSelectionState,
} from "@tanstack/react-table" } from "@medusajs/ui"
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { UseFormReturn } from "react-hook-form" import { UseFormReturn } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { keepPreviousData } from "@tanstack/react-query"
import { DataTable } from "../../../../../../../components/data-table"
import * as hooks from "../../../../../../../components/data-table/helpers/sales-channels"
import { import {
StackedFocusModal, StackedFocusModal,
useStackedModal, useStackedModal,
} from "../../../../../../../components/modals" } from "../../../../../../../components/modals"
import { _DataTable } from "../../../../../../../components/table/data-table"
import { useSalesChannels } from "../../../../../../../hooks/api/sales-channels" import { useSalesChannels } from "../../../../../../../hooks/api/sales-channels"
import { useSalesChannelTableColumns } from "../../../../../../../hooks/table/columns/use-sales-channel-table-columns"
import { useSalesChannelTableFilters } from "../../../../../../../hooks/table/filters/use-sales-channel-table-filters"
import { useSalesChannelTableQuery } from "../../../../../../../hooks/table/query/use-sales-channel-table-query"
import { useDataTable } from "../../../../../../../hooks/use-data-table"
import { ProductCreateSchemaType } from "../../../../types" import { ProductCreateSchemaType } from "../../../../types"
import { SC_STACKED_MODAL_ID } from "../../constants" import { SC_STACKED_MODAL_ID } from "../../constants"
@@ -35,16 +32,19 @@ export const ProductCreateSalesChannelStackedModal = ({
const { getValues, setValue } = form const { getValues, setValue } = form
const { setIsOpen, getIsOpen } = useStackedModal() const { setIsOpen, getIsOpen } = useStackedModal()
const [selection, setSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>(
{}
)
const [state, setState] = useState<{ id: string; name: string }[]>([]) const [state, setState] = useState<{ id: string; name: string }[]>([])
const { searchParams, raw } = useSalesChannelTableQuery({ const searchParams = hooks.useSalesChannelTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
prefix: SC_STACKED_MODAL_ID, prefix: SC_STACKED_MODAL_ID,
}) })
const { sales_channels, count, isLoading, isError, error } = useSalesChannels( const { sales_channels, count, isLoading, isError, error } = useSalesChannels(
searchParams,
{ {
...searchParams, placeholderData: keepPreviousData,
} }
) )
@@ -65,7 +65,7 @@ export const ProductCreateSalesChannelStackedModal = ({
})) }))
) )
setSelection( setRowSelection(
salesChannels.reduce( salesChannels.reduce(
(acc, channel) => ({ (acc, channel) => ({
...acc, ...acc,
@@ -77,11 +77,12 @@ export const ProductCreateSalesChannelStackedModal = ({
} }
}, [open, getValues]) }, [open, getValues])
const updater: OnChangeFn<RowSelectionState> = (fn) => { const onRowSelectionChange = (state: DataTableRowSelectionState) => {
const value = typeof fn === "function" ? fn(selection) : fn const ids = Object.keys(state)
const ids = Object.keys(value)
const addedIdsSet = new Set(ids.filter((id) => value[id] && !selection[id])) const addedIdsSet = new Set(
ids.filter((id) => state[id] && !rowSelection[id])
)
let addedSalesChannels: { id: string; name: string }[] = [] let addedSalesChannels: { id: string; name: string }[] = []
@@ -91,10 +92,10 @@ export const ProductCreateSalesChannelStackedModal = ({
} }
setState((prev) => { setState((prev) => {
const filteredPrev = prev.filter((channel) => value[channel.id]) const filteredPrev = prev.filter((channel) => state[channel.id])
return Array.from(new Set([...filteredPrev, ...addedSalesChannels])) return Array.from(new Set([...filteredPrev, ...addedSalesChannels]))
}) })
setSelection(value) setRowSelection(state)
} }
const handleAdd = () => { const handleAdd = () => {
@@ -105,23 +106,9 @@ export const ProductCreateSalesChannelStackedModal = ({
setIsOpen(SC_STACKED_MODAL_ID, false) setIsOpen(SC_STACKED_MODAL_ID, false)
} }
const filters = useSalesChannelTableFilters() const filters = hooks.useSalesChannelTableFilters()
const columns = useColumns() const columns = useColumns()
const emptyState = hooks.useSalesChannelTableEmptyState()
const { table } = useDataTable({
data: sales_channels ?? [],
columns,
count: sales_channels?.length ?? 0,
enablePagination: true,
enableRowSelection: true,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
rowSelection: {
state: selection,
updater,
},
prefix: SC_STACKED_MODAL_ID,
})
if (isError) { if (isError) {
throw error throw error
@@ -131,22 +118,20 @@ export const ProductCreateSalesChannelStackedModal = ({
<StackedFocusModal.Content className="flex flex-col overflow-hidden"> <StackedFocusModal.Content className="flex flex-col overflow-hidden">
<StackedFocusModal.Header /> <StackedFocusModal.Header />
<StackedFocusModal.Body className="flex-1 overflow-hidden"> <StackedFocusModal.Body className="flex-1 overflow-hidden">
<_DataTable <DataTable
table={table} data={sales_channels}
columns={columns} columns={columns}
pageSize={PAGE_SIZE}
filters={filters} filters={filters}
emptyState={emptyState}
rowCount={count}
pageSize={PAGE_SIZE}
getRowId={(row) => row.id}
rowSelection={{
state: rowSelection,
onRowSelectionChange,
}}
isLoading={isLoading} isLoading={isLoading}
layout="fill" layout="fill"
orderBy={[
{ key: "name", label: t("fields.name") },
{ key: "created_at", label: t("fields.createdAt") },
{ key: "updated_at", label: t("fields.updatedAt") },
]}
queryObject={raw}
search
pagination
count={count}
prefix={SC_STACKED_MODAL_ID} prefix={SC_STACKED_MODAL_ID}
/> />
</StackedFocusModal.Body> </StackedFocusModal.Body>
@@ -166,44 +151,10 @@ export const ProductCreateSalesChannelStackedModal = ({
) )
} }
const columnHelper = const columnHelper = createDataTableColumnHelper<HttpTypes.AdminSalesChannel>()
createColumnHelper<AdminSalesChannelResponse["sales_channel"]>()
const useColumns = () => { const useColumns = () => {
const base = useSalesChannelTableColumns() const base = hooks.useSalesChannelTableColumns()
return useMemo( return useMemo(() => [columnHelper.select(), ...base], [base])
() => [
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()
}}
/>
)
},
}),
...base,
],
[base]
)
} }
@@ -18,7 +18,8 @@ import { useTranslation } from "react-i18next"
import { CellContext } from "@tanstack/react-table" import { CellContext } from "@tanstack/react-table"
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 { useDataTableDateFilters } from "../../../../../components/data-table/hooks/general/use-data-table-date-filters" import { useDataTableDateColumns } from "../../../../../components/data-table/helpers/general/use-data-table-date-columns"
import { useDataTableDateFilters } from "../../../../../components/data-table/helpers/general/use-data-table-date-filters"
import { import {
useDeleteVariantLazy, useDeleteVariantLazy,
useProductVariants, useProductVariants,
@@ -144,6 +145,8 @@ const useColumns = (product: HttpTypes.AdminProduct) => {
const { mutateAsync } = useDeleteVariantLazy(product.id) const { mutateAsync } = useDeleteVariantLazy(product.id)
const prompt = usePrompt() const prompt = usePrompt()
const dateColumns = useDataTableDateColumns<HttpTypes.AdminProductVariant>()
const handleDelete = useCallback( const handleDelete = useCallback(
async (id: string, title: string) => { async (id: string, title: string) => {
const res = await prompt({ const res = await prompt({
@@ -330,25 +333,28 @@ const useColumns = (product: HttpTypes.AdminProduct) => {
const { text, hasInventoryKit, quantity } = getInventory(row.original) const { text, hasInventoryKit, quantity } = getInventory(row.original)
return ( return (
<div className="flex h-full w-full items-center gap-2 overflow-hidden"> <Tooltip content={text}>
{hasInventoryKit && <Component />} <div className="flex h-full w-full items-center gap-2 overflow-hidden">
<span {hasInventoryKit && <Component />}
className={clx("truncate", { <span
"text-ui-fg-error": !quantity, className={clx("truncate", {
})} "text-ui-fg-error": !quantity,
title={text} })}
> >
{text} {text}
</span> </span>
</div> </div>
</Tooltip>
) )
}, },
maxSize: 250,
}), }),
...dateColumns,
columnHelper.action({ columnHelper.action({
actions: getActions, actions: getActions,
}), }),
] ]
}, [t, optionColumns, getActions, getInventory]) }, [t, optionColumns, dateColumns, getActions, getInventory])
} }
const filterHelper = const filterHelper =
@@ -1,5 +1,5 @@
import { Button, Checkbox } from "@medusajs/ui" import { Button, createDataTableColumnHelper } from "@medusajs/ui"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table" import { RowSelectionState } from "@tanstack/react-table"
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
@@ -8,17 +8,14 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { keepPreviousData } from "@tanstack/react-query" import { keepPreviousData } from "@tanstack/react-query"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { DataTable } from "../../../../../components/data-table"
import * as hooks from "../../../../../components/data-table/helpers/sales-channels"
import { import {
RouteFocusModal, RouteFocusModal,
useRouteModal, useRouteModal,
} from "../../../../../components/modals" } from "../../../../../components/modals"
import { _DataTable } from "../../../../../components/table/data-table"
import { useUpdateProduct } from "../../../../../hooks/api/products" import { useUpdateProduct } from "../../../../../hooks/api/products"
import { useSalesChannels } from "../../../../../hooks/api/sales-channels" import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/use-sales-channel-table-columns"
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters"
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
type EditSalesChannelsFormProps = { type EditSalesChannelsFormProps = {
product: HttpTypes.AdminProduct product: HttpTypes.AdminProduct
@@ -29,6 +26,7 @@ const EditSalesChannelsSchema = zod.object({
}) })
const PAGE_SIZE = 50 const PAGE_SIZE = 50
const PREFIX = "sc"
export const EditSalesChannelsForm = ({ export const EditSalesChannelsForm = ({
product, product,
@@ -62,8 +60,9 @@ export const EditSalesChannelsForm = ({
}) })
}, [rowSelection, setValue]) }, [rowSelection, setValue])
const { searchParams, raw } = useSalesChannelTableQuery({ const searchParams = hooks.useSalesChannelTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
prefix: PREFIX,
}) })
const { sales_channels, count, isLoading, isError, error } = useSalesChannels( const { sales_channels, count, isLoading, isError, error } = useSalesChannels(
{ {
@@ -74,23 +73,10 @@ export const EditSalesChannelsForm = ({
} }
) )
const filters = useSalesChannelTableFilters() const filters = hooks.useSalesChannelTableFilters()
const emptyState = hooks.useSalesChannelTableEmptyState()
const columns = useColumns() const columns = useColumns()
const { table } = useDataTable({
data: sales_channels ?? [],
columns,
count,
enablePagination: true,
enableRowSelection: true,
rowSelection: {
state: rowSelection,
updater: setRowSelection,
},
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
const { mutateAsync, isPending: isMutating } = useUpdateProduct(product.id) const { mutateAsync, isPending: isMutating } = useUpdateProduct(product.id)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
@@ -123,22 +109,21 @@ export const EditSalesChannelsForm = ({
<div className="flex h-full flex-col overflow-hidden"> <div className="flex h-full flex-col overflow-hidden">
<RouteFocusModal.Header /> <RouteFocusModal.Header />
<RouteFocusModal.Body className="flex-1 overflow-hidden"> <RouteFocusModal.Body className="flex-1 overflow-hidden">
<_DataTable <DataTable
table={table} data={sales_channels}
columns={columns} columns={columns}
pageSize={PAGE_SIZE} getRowId={(row) => row.id}
rowCount={count}
isLoading={isLoading} isLoading={isLoading}
count={count}
filters={filters} filters={filters}
search="autofocus" rowSelection={{
pagination state: rowSelection,
orderBy={[ onRowSelectionChange: setRowSelection,
{ key: "name", label: t("fields.name") }, }}
{ key: "created_at", label: t("fields.createdAt") }, autoFocusSearch
{ key: "updated_at", label: t("fields.updatedAt") },
]}
queryObject={raw}
layout="fill" layout="fill"
emptyState={emptyState}
prefix={PREFIX}
/> />
</RouteFocusModal.Body> </RouteFocusModal.Body>
<RouteFocusModal.Footer> <RouteFocusModal.Footer>
@@ -159,43 +144,12 @@ export const EditSalesChannelsForm = ({
} }
const columnHelper = const columnHelper =
createColumnHelper<HttpTypes.AdminSalesChannelResponse["sales_channel"]>() createDataTableColumnHelper<
HttpTypes.AdminSalesChannelResponse["sales_channel"]
>()
const useColumns = () => { const useColumns = () => {
const columns = useSalesChannelTableColumns() const columns = hooks.useSalesChannelTableColumns()
return useMemo( return useMemo(() => [columnHelper.select(), ...columns], [columns])
() => [
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,32 +1,26 @@
import { PencilSquare, Trash } from "@medusajs/icons" import { PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types" import { HttpTypes } from "@medusajs/types"
import { import {
Button,
Container, Container,
Heading, createDataTableColumnHelper,
Text,
toast, toast,
usePrompt, usePrompt,
} from "@medusajs/ui" } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query" import { keepPreviousData } from "@tanstack/react-query"
import { createColumnHelper } from "@tanstack/react-table" import { useCallback, useMemo } from "react"
import { useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { import { DataTable } from "../../../../components/data-table"
ActionGroup, import * as hooks from "../../../../components/data-table/helpers/sales-channels"
ActionMenu,
} from "../../../../components/common/action-menu"
import { _DataTable } from "../../../../components/table/data-table"
import { useStore } from "../../../../hooks/api" import { useStore } from "../../../../hooks/api"
import { import {
useDeleteSalesChannel, useDeleteSalesChannelLazy,
useSalesChannels, useSalesChannels,
} from "../../../../hooks/api/sales-channels" } from "../../../../hooks/api/sales-channels"
import { useSalesChannelTableColumns } from "../../../../hooks/table/columns/use-sales-channel-table-columns"
import { useSalesChannelTableFilters } from "../../../../hooks/table/filters" type SalesChannelWithIsDefault = HttpTypes.AdminSalesChannel & {
import { useSalesChannelTableQuery } from "../../../../hooks/table/query/use-sales-channel-table-query" is_default?: boolean
import { useDataTable } from "../../../../hooks/use-data-table" }
const PAGE_SIZE = 20 const PAGE_SIZE = 20
@@ -35,157 +29,130 @@ export const SalesChannelListTable = () => {
const { store } = useStore() const { store } = useStore()
const { raw, searchParams } = useSalesChannelTableQuery({ const searchParams = hooks.useSalesChannelTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
}) })
const { const { sales_channels, count, isPending, isError, error } = useSalesChannels(
sales_channels, searchParams,
count, {
isPending: isLoading, placeholderData: keepPreviousData,
isError, }
error, )
} = useSalesChannels(searchParams, {
placeholderData: keepPreviousData,
}) as Omit<ReturnType<typeof useSalesChannels>, "sales_channels"> & {
sales_channels: (HttpTypes.AdminSalesChannel & { is_default?: boolean })[]
}
const columns = useColumns() const columns = useColumns()
const filters = useSalesChannelTableFilters() const filters = hooks.useSalesChannelTableFilters()
const emptyState = hooks.useSalesChannelTableEmptyState()
const sales_channels_data = const sales_channels_data: SalesChannelWithIsDefault[] =
sales_channels?.map((sales_channel) => { sales_channels?.map((sales_channel) => {
sales_channel.is_default = return {
store?.default_sales_channel_id === sales_channel.id ...sales_channel,
return sales_channel is_default: store?.default_sales_channel_id === sales_channel.id,
}
}) ?? [] }) ?? []
const { table } = useDataTable({
data: sales_channels_data,
columns,
count,
enablePagination: true,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<Container className="divide-y p-0"> <Container className="p-0">
<div className="flex items-center justify-between px-6 py-4"> <DataTable
<div> data={sales_channels_data}
<Heading>{t("salesChannels.domain")}</Heading>
<Text className="text-ui-fg-subtle" size="small">
{t("salesChannels.subtitle")}
</Text>
</div>
<Link to="/settings/sales-channels/create">
<Button size="small" variant="secondary">
{t("actions.create")}
</Button>
</Link>
</div>
<_DataTable
table={table}
columns={columns} columns={columns}
count={count} rowCount={count}
getRowId={(row) => row.id}
pageSize={PAGE_SIZE} pageSize={PAGE_SIZE}
filters={filters} filters={filters}
pagination isLoading={isPending}
search emptyState={emptyState}
navigateTo={(row) => row.id} heading={t("salesChannels.domain")}
isLoading={isLoading} subHeading={t("salesChannels.subtitle")}
queryObject={raw} action={{
orderBy={[ label: t("actions.create"),
{ key: "name", label: t("fields.name") }, to: "/settings/sales-channels/create",
{ key: "created_at", label: t("fields.createdAt") }, }}
{ key: "updated_at", label: t("fields.updatedAt") }, rowHref={(row) => `/settings/sales-channels/${row.id}`}
]}
/> />
</Container> </Container>
) )
} }
const SalesChannelActions = ({ const columnHelper = createDataTableColumnHelper<
salesChannel, HttpTypes.AdminSalesChannel & { is_default?: boolean }
}: { >()
salesChannel: HttpTypes.AdminSalesChannel & { is_default?: boolean }
}) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useDeleteSalesChannel(salesChannel.id)
const handleDelete = async () => {
const confirm = await prompt({
title: t("general.areYouSure"),
description: t("salesChannels.deleteSalesChannelWarning", {
name: salesChannel.name,
}),
verificationInstruction: t("general.typeToConfirm"),
verificationText: salesChannel.name,
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!confirm) {
return
}
await mutateAsync(undefined, {
onSuccess: () => {
toast.success(t("salesChannels.toast.delete"))
},
onError: (e) => {
toast.error(e.message)
},
})
}
const disabledTooltip = salesChannel.is_default
? t("salesChannels.tooltip.cannotDeleteDefault")
: undefined
const groups: ActionGroup[] = [
{
actions: [
{
icon: <PencilSquare />,
label: t("actions.edit"),
to: `/settings/sales-channels/${salesChannel.id}/edit`,
},
{
icon: <Trash />,
label: t("actions.delete"),
onClick: handleDelete,
disabled: salesChannel.is_default,
disabledTooltip,
},
],
},
]
return <ActionMenu groups={groups} />
}
const columnHelper = createColumnHelper<HttpTypes.AdminSalesChannel>()
const useColumns = () => { const useColumns = () => {
const base = useSalesChannelTableColumns() const { t } = useTranslation()
const prompt = usePrompt()
const navigate = useNavigate()
const base = hooks.useSalesChannelTableColumns()
const { mutateAsync } = useDeleteSalesChannelLazy()
const handleDelete = useCallback(
async (salesChannel: HttpTypes.AdminSalesChannel) => {
const confirm = await prompt({
title: t("general.areYouSure"),
description: t("salesChannels.deleteSalesChannelWarning", {
name: salesChannel.name,
}),
verificationInstruction: t("general.typeToConfirm"),
verificationText: salesChannel.name,
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!confirm) {
return
}
await mutateAsync(salesChannel.id, {
onSuccess: () => {
toast.success(t("salesChannels.toast.delete"))
},
onError: (e) => {
toast.error(e.message)
},
})
},
[t, prompt, mutateAsync]
)
return useMemo( return useMemo(
() => [ () => [
...base, ...base,
columnHelper.display({ columnHelper.action({
id: "actions", actions: (ctx) => {
cell: ({ row }) => { const disabledTooltip = ctx.row.original.is_default
return <SalesChannelActions salesChannel={row.original} /> ? t("salesChannels.tooltip.cannotDeleteDefault")
: undefined
return [
[
{
icon: <PencilSquare />,
label: t("actions.edit"),
onClick: () =>
navigate(
`/settings/sales-channels/${ctx.row.original.id}/edit`
),
},
],
[
{
icon: <Trash />,
label: t("actions.delete"),
onClick: () => handleDelete(ctx.row.original),
disabled: ctx.row.original.is_default,
disabledTooltip,
},
],
]
}, },
}), }),
], ],
[base] [base, handleDelete, navigate, t]
) )
} }
@@ -7,9 +7,9 @@ import { useNavigate } from "react-router-dom"
import { PencilSquare } from "@medusajs/icons" import { PencilSquare } from "@medusajs/icons"
import { DataTable } from "../../../../../components/data-table" import { DataTable } from "../../../../../components/data-table"
import { useDataTableDateFilters } from "../../../../../components/data-table/hooks/general/use-data-table-date-filters" import { useDataTableDateColumns } from "../../../../../components/data-table/helpers/general/use-data-table-date-columns"
import { useDataTableDateFilters } from "../../../../../components/data-table/helpers/general/use-data-table-date-filters"
import { useUsers } from "../../../../../hooks/api/users" import { useUsers } from "../../../../../hooks/api/users"
import { useDate } from "../../../../../hooks/use-date"
import { useQueryParams } from "../../../../../hooks/use-query-params" import { useQueryParams } from "../../../../../hooks/use-query-params"
const PAGE_SIZE = 20 const PAGE_SIZE = 20
@@ -73,7 +73,8 @@ const columnHelper = createDataTableColumnHelper<HttpTypes.AdminUser>()
const useColumns = () => { const useColumns = () => {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const { getFullDate } = useDate()
const dateColumns = useDataTableDateColumns<HttpTypes.AdminUser>()
return useMemo( return useMemo(
() => [ () => [
@@ -104,24 +105,7 @@ const useColumns = () => {
sortAscLabel: t("filters.sorting.alphabeticallyAsc"), sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"), sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
}), }),
columnHelper.accessor("created_at", { ...dateColumns,
header: t("fields.createdAt"),
cell: ({ row }) => {
return getFullDate({ date: row.original.created_at })
},
enableSorting: true,
sortAscLabel: t("filters.sorting.dateAsc"),
sortDescLabel: t("filters.sorting.dateDesc"),
}),
columnHelper.accessor("updated_at", {
header: t("fields.updatedAt"),
cell: ({ row }) => {
return getFullDate({ date: row.original.updated_at })
},
enableSorting: true,
sortAscLabel: t("filters.sorting.dateAsc"),
sortDescLabel: t("filters.sorting.dateDesc"),
}),
columnHelper.action({ columnHelper.action({
actions: [ actions: [
{ {
@@ -134,7 +118,7 @@ const useColumns = () => {
], ],
}), }),
], ],
[t, getFullDate, navigate] [t, navigate, dateColumns]
) )
} }
@@ -27,7 +27,7 @@ interface DataTableTableProps {
* This component renders the table in a data table, supporting advanced features. * This component renders the table in a data table, supporting advanced features.
*/ */
const DataTableTable = (props: DataTableTableProps) => { const DataTableTable = (props: DataTableTableProps) => {
const [hoveredRowId, setHoveredRowId] = React.useState<string | null>(null) const hoveredRowId = React.useRef<string | null>(null)
const isKeyDown = React.useRef(false) const isKeyDown = React.useRef(false)
const [showStickyBorder, setShowStickyBorder] = React.useState(false) const [showStickyBorder, setShowStickyBorder] = React.useState(false)
@@ -57,7 +57,7 @@ const DataTableTable = (props: DataTableTableProps) => {
const row = instance const row = instance
.getRowModel() .getRowModel()
.rows.find((r) => r.id === hoveredRowId) .rows.find((r) => r.id === hoveredRowId.current)
if (row && row.getCanSelect()) { if (row && row.getCanSelect()) {
row.toggleSelected() row.toggleSelected()
@@ -190,10 +190,10 @@ const DataTableTable = (props: DataTableTableProps) => {
return ( return (
<Table.Row <Table.Row
key={row.id} key={row.id}
onMouseEnter={() => setHoveredRowId(row.id)} onMouseEnter={() => (hoveredRowId.current = row.id)}
onMouseLeave={() => setHoveredRowId(null)} onMouseLeave={() => (hoveredRowId.current = null)}
onClick={(e) => instance.onRowClick?.(e, row)} onClick={(e) => instance.onRowClick?.(e, row)}
className={clx("group/row last:border-b-0", { className={clx("group/row last-of-type:border-b-0", {
"cursor-pointer": !!instance.onRowClick, "cursor-pointer": !!instance.onRowClick,
})} })}
> >
@@ -288,7 +288,7 @@ const DataTableEmptyStateDisplay = ({
state === DataTableEmptyState.EMPTY ? props?.empty : props?.filtered state === DataTableEmptyState.EMPTY ? props?.empty : props?.filtered
return ( return (
<div className="flex min-h-[250px] w-full flex-col items-center justify-center border-y px-6 py-4"> <div className="flex min-h-[250px] w-full flex-1 flex-col items-center justify-center border-y px-6 py-4">
{content?.custom ?? ( {content?.custom ?? (
<DefaultEmptyStateContent <DefaultEmptyStateContent
heading={content?.heading} heading={content?.heading}
@@ -332,4 +332,3 @@ function getIsEditableElementFocused() {
export { DataTableTable } export { DataTableTable }
export type { DataTableEmptyStateProps, DataTableTableProps } export type { DataTableEmptyStateProps, DataTableTableProps }
@@ -19,6 +19,8 @@ export type {
DataTableFilteringState, DataTableFilteringState,
DataTableHeaderContext, DataTableHeaderContext,
DataTablePaginationState, DataTablePaginationState,
DataTableRow,
DataTableRowData,
DataTableRowSelectionState, DataTableRowSelectionState,
DataTableSortDirection, DataTableSortDirection,
DataTableSortingState, DataTableSortingState,
@@ -14,6 +14,7 @@ import type {
IdentifiedColumnDef, IdentifiedColumnDef,
IdIdentifier, IdIdentifier,
PaginationState, PaginationState,
Row,
RowData, RowData,
RowSelectionState, RowSelectionState,
SortDirection, SortDirection,
@@ -23,6 +24,8 @@ import type {
export type DataTableRowData = RowData export type DataTableRowData = RowData
export type DataTableRow<TData extends DataTableRowData> = Row<TData>
export type DataTableAction<TData> = { export type DataTableAction<TData> = {
label: string label: string
onClick: (ctx: CellContext<TData, unknown>) => void onClick: (ctx: CellContext<TData, unknown>) => void
@@ -21,6 +21,7 @@ import {
DataTableFilteringState, DataTableFilteringState,
DataTableFilterOption, DataTableFilterOption,
DataTablePaginationState, DataTablePaginationState,
DataTableRow,
DataTableRowSelectionState, DataTableRowSelectionState,
DataTableSortingState, DataTableSortingState,
} from "./types"; } from "./types";
@@ -56,6 +57,7 @@ interface DataTableOptions<TData>
rowSelection?: { rowSelection?: {
state: DataTableRowSelectionState state: DataTableRowSelectionState
onRowSelectionChange: (state: DataTableRowSelectionState) => void onRowSelectionChange: (state: DataTableRowSelectionState) => void
enableRowSelection?: boolean | ((row: DataTableRow<TData>) => boolean) | undefined
} }
/** /**
* The state and callback for the sorting. * The state and callback for the sorting.
@@ -170,7 +172,7 @@ const useDataTable = <TData,>({
const { state: sortingState, onSortingChange } = sorting ?? {} const { state: sortingState, onSortingChange } = sorting ?? {}
const { state: filteringState, onFilteringChange } = filtering ?? {} const { state: filteringState, onFilteringChange } = filtering ?? {}
const { state: paginationState, onPaginationChange } = pagination ?? {} const { state: paginationState, onPaginationChange } = pagination ?? {}
const { state: rowSelectionState, onRowSelectionChange } = rowSelection ?? {} const { state: rowSelectionState, onRowSelectionChange, enableRowSelection } = rowSelection ?? {}
const autoResetPageIndexHandler = React.useCallback(() => { const autoResetPageIndexHandler = React.useCallback(() => {
return autoResetPageIndex return autoResetPageIndex
@@ -236,6 +238,7 @@ const useDataTable = <TData,>({
), ),
pagination: paginationState, pagination: paginationState,
}, },
enableRowSelection,
rowCount, rowCount,
onColumnFiltersChange: filteringStateHandler(), onColumnFiltersChange: filteringStateHandler(),
onRowSelectionChange: rowSelectionStateHandler(), onRowSelectionChange: rowSelectionStateHandler(),
+12 -12
View File
@@ -5294,7 +5294,7 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@medusajs/admin-vite-plugin@2.4.0, @medusajs/admin-vite-plugin@workspace:packages/admin/admin-vite-plugin": "@medusajs/admin-vite-plugin@2.4.0, @medusajs/admin-vite-plugin@workspace:packages/admin/admin-vite-plugin, @medusajs/admin-vite-plugin@~2.4.0":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@medusajs/admin-vite-plugin@workspace:packages/admin/admin-vite-plugin" resolution: "@medusajs/admin-vite-plugin@workspace:packages/admin/admin-vite-plugin"
dependencies: dependencies:
@@ -5605,12 +5605,12 @@ __metadata:
"@hookform/error-message": ^2.0.1 "@hookform/error-message": ^2.0.1
"@hookform/resolvers": 3.4.2 "@hookform/resolvers": 3.4.2
"@medusajs/admin-shared": ~2.4.0 "@medusajs/admin-shared": ~2.4.0
"@medusajs/admin-vite-plugin": 2.4.0 "@medusajs/admin-vite-plugin": ~2.4.0
"@medusajs/icons": 2.4.0 "@medusajs/icons": ~2.4.0
"@medusajs/js-sdk": 2.4.0 "@medusajs/js-sdk": ~2.4.0
"@medusajs/types": 2.4.0 "@medusajs/types": ~2.4.0
"@medusajs/ui": 4.0.4 "@medusajs/ui": ~4.0.4
"@medusajs/ui-preset": 2.4.0 "@medusajs/ui-preset": ~2.4.0
"@tanstack/react-query": 5.64.2 "@tanstack/react-query": 5.64.2
"@tanstack/react-table": 8.20.5 "@tanstack/react-table": 8.20.5
"@tanstack/react-virtual": ^3.8.3 "@tanstack/react-virtual": ^3.8.3
@@ -5848,7 +5848,7 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@medusajs/icons@2.4.0, @medusajs/icons@workspace:packages/design-system/icons, @medusajs/icons@~2.4.0": "@medusajs/icons@workspace:packages/design-system/icons, @medusajs/icons@~2.4.0":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@medusajs/icons@workspace:packages/design-system/icons" resolution: "@medusajs/icons@workspace:packages/design-system/icons"
dependencies: dependencies:
@@ -5934,7 +5934,7 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@medusajs/js-sdk@2.4.0, @medusajs/js-sdk@workspace:packages/core/js-sdk": "@medusajs/js-sdk@workspace:packages/core/js-sdk, @medusajs/js-sdk@~2.4.0":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@medusajs/js-sdk@workspace:packages/core/js-sdk" resolution: "@medusajs/js-sdk@workspace:packages/core/js-sdk"
dependencies: dependencies:
@@ -6631,7 +6631,7 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@medusajs/types@2.4.0, @medusajs/types@workspace:^, @medusajs/types@workspace:packages/core/types, @medusajs/types@~2.4.0": "@medusajs/types@workspace:^, @medusajs/types@workspace:packages/core/types, @medusajs/types@~2.4.0":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@medusajs/types@workspace:packages/core/types" resolution: "@medusajs/types@workspace:packages/core/types"
dependencies: dependencies:
@@ -6654,7 +6654,7 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@medusajs/ui-preset@2.4.0, @medusajs/ui-preset@workspace:packages/design-system/ui-preset, @medusajs/ui-preset@~2.4.0": "@medusajs/ui-preset@workspace:packages/design-system/ui-preset, @medusajs/ui-preset@~2.4.0":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@medusajs/ui-preset@workspace:packages/design-system/ui-preset" resolution: "@medusajs/ui-preset@workspace:packages/design-system/ui-preset"
dependencies: dependencies:
@@ -6669,7 +6669,7 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@medusajs/ui@4.0.4, @medusajs/ui@workspace:packages/design-system/ui": "@medusajs/ui@workspace:packages/design-system/ui, @medusajs/ui@~4.0.4":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@medusajs/ui@workspace:packages/design-system/ui" resolution: "@medusajs/ui@workspace:packages/design-system/ui"
dependencies: dependencies: