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

This commit is contained in:
Kasper Fabricius Kristensen
2025-02-07 15:26:49 +01:00
committed by GitHub
parent d588073cea
commit fcd3e2226e
34 changed files with 847 additions and 858 deletions
@@ -1,270 +1,205 @@
import { PencilSquare, Plus, Trash } from "@medusajs/icons"
import { AdminApiKeyResponse, AdminSalesChannelResponse } from "@medusajs/types"
import { Checkbox, Container, Heading, toast, usePrompt } from "@medusajs/ui"
import { PencilSquare, Trash } from "@medusajs/icons"
import { AdminApiKeyResponse, HttpTypes } from "@medusajs/types"
import {
Container,
createDataTableColumnHelper,
createDataTableCommandHelper,
DataTableRowSelectionState,
toast,
usePrompt,
} from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import { useMemo, useState } from "react"
import { RowSelectionState } from "@tanstack/react-table"
import { useCallback, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { _DataTable } from "../../../../../components/table/data-table"
import { useNavigate } from "react-router-dom"
import { DataTable } from "../../../../../components/data-table"
import * as hooks from "../../../../../components/data-table/helpers/sales-channels"
import { useBatchRemoveSalesChannelsFromApiKey } from "../../../../../hooks/api/api-keys"
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 = {
apiKey: AdminApiKeyResponse["api_key"]
}
const PAGE_SIZE = 10
const PREFIX = "sc"
export const ApiKeySalesChannelSection = ({
apiKey,
}: ApiKeySalesChannelSectionProps) => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const { t } = useTranslation()
const prompt = usePrompt()
const { raw, searchParams } = useSalesChannelTableQuery({
const searchParams = hooks.useSalesChannelTableQuery({
pageSize: PAGE_SIZE,
prefix: PREFIX,
})
const { sales_channels, count, isLoading } = useSalesChannels(
const { sales_channels, count, isPending } = useSalesChannels(
{ ...searchParams, publishable_key_id: apiKey.id },
{
placeholderData: keepPreviousData,
}
)
const columns = useColumns()
const filters = useSalesChannelTableFilters()
const { table } = useDataTable({
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)
},
})
}
const columns = useColumns(apiKey.id)
const filters = hooks.useSalesChannelTableFilters()
const commands = useCommands(apiKey.id, setRowSelection)
const emptyState = hooks.useSalesChannelTableEmptyState()
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("salesChannels.domain")}</Heading>
<ActionMenu
groups={[
{
actions: [
{
icon: <Plus />,
label: t("actions.add"),
to: "sales-channels",
},
],
},
]}
/>
</div>
<_DataTable
table={table}
<DataTable
data={sales_channels}
columns={columns}
filters={filters}
count={count}
isLoading={isLoading}
queryObject={raw}
navigateTo={(row) => `/settings/sales-channels/${row.id}`}
orderBy={[
{
key: "name",
label: t("fields.name"),
},
{
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"),
commands={commands}
heading={t("salesChannels.domain")}
getRowId={(row) => row.id}
rowCount={count}
isLoading={isPending}
emptyState={emptyState}
rowSelection={{
state: rowSelection,
onRowSelectionChange: setRowSelection,
}}
rowHref={(row) => `/settings/sales-channels/${row.id}`}
action={{
label: t("actions.add"),
to: "sales-channels",
}}
prefix={PREFIX}
pageSize={PAGE_SIZE}
/>
</Container>
)
}
const SalesChannelActions = ({
salesChannel,
apiKey,
}: {
salesChannel: AdminSalesChannelResponse["sales_channel"]
apiKey: string
}) => {
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminSalesChannel>()
const useColumns = (id: string) => {
const { t } = useTranslation()
const navigate = useNavigate()
const prompt = usePrompt()
const { mutateAsync } = useBatchRemoveSalesChannelsFromApiKey(apiKey)
const base = hooks.useSalesChannelTableColumns()
const handleDelete = async () => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("apiKeyManagement.removeSalesChannel.warning", {
name: salesChannel.name,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
const { mutateAsync } = useBatchRemoveSalesChannelsFromApiKey(id)
if (!res) {
return
}
const handleDelete = useCallback(
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], {
onSuccess: () => {
toast.success(
t("apiKeyManagement.removeSalesChannel.successToast", {
count: 1,
})
)
},
onError: (err) => {
toast.error(err.message)
},
})
}
if (!res) {
return
}
return (
<ActionMenu
groups={[
{
actions: [
{
icon: <PencilSquare />,
label: t("actions.edit"),
to: `/settings/sales-channels/${salesChannel.id}/edit`,
},
],
await mutateAsync([salesChannel.id], {
onSuccess: () => {
toast.success(
t("apiKeyManagement.removeSalesChannel.successToast", {
count: 1,
})
)
},
{
actions: [
{
icon: <Trash />,
label: t("actions.delete"),
onClick: handleDelete,
},
],
onError: (err) => {
toast.error(err.message)
},
]}
/>
})
},
[mutateAsync, prompt, t]
)
}
const columnHelper =
createColumnHelper<AdminSalesChannelResponse["sales_channel"]>()
const useColumns = () => {
const base = useSalesChannelTableColumns()
return useMemo(
() => [
columnHelper.display({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
cell: ({ row }) => {
return (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
)
},
}),
columnHelper.select(),
...base,
columnHelper.display({
id: "actions",
cell: ({ row, table }) => {
const { apiKey } = table.options.meta as {
apiKey: string
}
return (
<SalesChannelActions salesChannel={row.original} apiKey={apiKey} />
)
},
columnHelper.action({
actions: (ctx) => [
[
{
label: t("actions.edit"),
icon: <PencilSquare />,
onClick: () => {
navigate(`/settings/sales-channels/${ctx.row.original.id}/edit`)
},
},
],
[
{
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 { AdminSalesChannelResponse } from "@medusajs/types"
import { Button, Checkbox, Hint, Tooltip, toast } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { HttpTypes } from "@medusajs/types"
import {
OnChangeFn,
RowSelectionState,
createColumnHelper,
} from "@tanstack/react-table"
Button,
Checkbox,
DataTableRowSelectionState,
Hint,
createDataTableColumnHelper,
toast,
} from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { RowSelectionState } from "@tanstack/react-table"
import { useMemo, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
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 {
RouteFocusModal,
useRouteModal,
} 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 { useBatchAddSalesChannelsToApiKey } from "../../../../../hooks/api/api-keys"
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 = {
apiKey: string
@@ -31,10 +33,11 @@ type ApiKeySalesChannelFormProps = {
}
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 PREFIX = "sc_add"
export const ApiKeySalesChannelsForm = ({
apiKey,
@@ -54,51 +57,36 @@ export const ApiKeySalesChannelsForm = ({
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,
prefix: PREFIX,
})
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 },
{
placeholderData: keepPreviousData,
}
)
const updater: OnChangeFn<RowSelectionState> = (fn) => {
const state = typeof fn === "function" ? fn(rowSelection) : fn
const ids = Object.keys(state)
const updater = (selection: DataTableRowSelectionState) => {
const ids = Object.keys(selection)
setValue("sales_channel_ids", ids, {
shouldDirty: 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) => {
await mutateAsync(values.sales_channel_ids, {
onSuccess: () => {
@@ -139,27 +127,23 @@ export const ApiKeySalesChannelsForm = ({
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-auto">
<_DataTable
table={table}
<DataTable
data={sales_channels}
columns={columns}
count={count}
pageSize={PAGE_SIZE}
filters={filters}
pagination
search="autofocus"
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") },
]}
getRowId={(row) => row.id}
rowCount={count}
layout="fill"
noRecords={{
message: t(
"apiKeyManagement.addSalesChannels.list.noRecordsMessage"
),
emptyState={emptyState}
isLoading={isPending}
rowSelection={{
state: rowSelection,
onRowSelectionChange: updater,
enableRowSelection: (row) => !preSelected.includes(row.id),
}}
prefix={PREFIX}
pageSize={PAGE_SIZE}
autoFocusSearch
/>
</RouteFocusModal.Body>
<RouteFocusModal.Footer>
@@ -169,7 +153,7 @@ export const ApiKeySalesChannelsForm = ({
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button size="small" type="submit" isLoading={isPending}>
<Button size="small" type="submit" isLoading={isMutating}>
{t("actions.save")}
</Button>
</div>
@@ -179,60 +163,36 @@ export const ApiKeySalesChannelsForm = ({
)
}
const columnHelper =
createColumnHelper<AdminSalesChannelResponse["sales_channel"]>()
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminSalesChannel>()
const useColumns = () => {
const { t } = useTranslation()
const base = useSalesChannelTableColumns()
const base = hooks.useSalesChannelTableColumns()
return useMemo(
() => [
columnHelper.display({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
columnHelper.select({
cell: ({ row }) => {
const isPreSelected = !row.getCanSelect()
const isSelected = row.getIsSelected() || isPreSelected
const Component = (
<Checkbox
checked={isSelected}
disabled={isPreSelected}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
return (
<ConditionalTooltip
content={t("apiKeyManagement.salesChannels.alreadyAddedTooltip")}
showTooltip={isPreSelected}
>
<div>
<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,
@@ -1,10 +1,10 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import {
Container,
createDataTableColumnHelper,
toast,
usePrompt,
Container,
createDataTableColumnHelper,
toast,
usePrompt,
} from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { useCallback, useMemo } from "react"
@@ -12,12 +12,12 @@ import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
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 { useDashboardExtension } from "../../../../../extensions"
import {
useCustomerGroups,
useDeleteCustomerGroupLazy,
useCustomerGroups,
useDeleteCustomerGroupLazy,
} from "../../../../../hooks/api"
import { useDate } from "../../../../../hooks/use-date"
import { useQueryParams } from "../../../../../hooks/use-query-params"
@@ -1,24 +1,27 @@
import { zodResolver } from "@hookform/resolvers/zod"
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 { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import { useEffect, useMemo, useState } from "react"
import { useMemo, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { DataTable } from "../../../../../components/data-table"
import * as hooks from "../../../../../components/data-table/helpers/sales-channels"
import {
RouteFocusModal,
useRouteModal,
} 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 { 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 = {
location: HttpTypes.AdminStockLocation
@@ -29,6 +32,7 @@ const EditSalesChannelsSchema = zod.object({
})
const PAGE_SIZE = 50
const PREFIX = "sc"
export const LocationEditSalesChannelsForm = ({
location,
@@ -45,28 +49,25 @@ export const LocationEditSalesChannelsForm = ({
const { setValue } = form
const initialState =
location.sales_channels?.reduce((acc, curr) => {
acc[curr.id] = true
return acc
}, {} as RowSelectionState) ?? {}
const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>(
getInitialState(location)
)
const [rowSelection, setRowSelection] =
useState<RowSelectionState>(initialState)
useEffect(() => {
const ids = Object.keys(rowSelection)
const onRowSelectionChange = (selection: DataTableRowSelectionState) => {
const ids = Object.keys(selection)
setValue("sales_channels", ids, {
shouldDirty: true,
shouldTouch: true,
})
}, [rowSelection, setValue])
setRowSelection(selection)
}
const { searchParams, raw } = useSalesChannelTableQuery({
const searchParams = hooks.useSalesChannelTableQuery({
pageSize: PAGE_SIZE,
prefix: PREFIX,
})
const { sales_channels, count, isLoading, isError, error } = useSalesChannels(
const { sales_channels, count, isPending, isError, error } = useSalesChannels(
{
...searchParams,
},
@@ -75,22 +76,9 @@ export const LocationEditSalesChannelsForm = ({
}
)
const filters = useSalesChannelTableFilters()
const filters = hooks.useSalesChannelTableFilters()
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 emptyState = hooks.useSalesChannelTableEmptyState()
const { mutateAsync, isPending: isMutating } =
useUpdateStockLocationSalesChannels(location.id)
@@ -123,80 +111,67 @@ export const LocationEditSalesChannelsForm = ({
return (
<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.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">
<RouteFocusModal.Close asChild>
<Button size="small" variant="secondary">
<Button size="small" variant="secondary" type="button">
{t("actions.cancel")}
</Button>
</RouteFocusModal.Close>
<Button size="small" isLoading={isMutating} onClick={handleSubmit}>
<Button size="small" isLoading={isMutating} type="submit">
{t("actions.save")}
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body>
<_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.Footer>
</KeyboundForm>
</RouteFocusModal.Form>
)
}
const columnHelper = createColumnHelper<HttpTypes.AdminSalesChannel>()
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminSalesChannel>()
const useColumns = () => {
const columns = useSalesChannelTableColumns()
const base = hooks.useSalesChannelTableColumns()
return useMemo(
() => [
columnHelper.display({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
cell: ({ row }) => {
return (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
)
},
}),
...columns,
],
[columns]
return useMemo(() => [columnHelper.select(), ...base], [base])
}
function getInitialState(location: HttpTypes.AdminStockLocation) {
return (
location.sales_channels?.reduce((acc, curr) => {
acc[curr.id] = true
return acc
}, {} as DataTableRowSelectionState) ?? {}
)
}
@@ -1,24 +1,21 @@
import { AdminSalesChannelResponse } from "@medusajs/types"
import { Button, Checkbox } from "@medusajs/ui"
import { HttpTypes } from "@medusajs/types"
import {
OnChangeFn,
RowSelectionState,
createColumnHelper,
} from "@tanstack/react-table"
Button,
createDataTableColumnHelper,
DataTableRowSelectionState,
} from "@medusajs/ui"
import { useEffect, useMemo, useState } from "react"
import { UseFormReturn } from "react-hook-form"
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 {
StackedFocusModal,
useStackedModal,
} from "../../../../../../../components/modals"
import { _DataTable } from "../../../../../../../components/table/data-table"
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 { SC_STACKED_MODAL_ID } from "../../constants"
@@ -35,16 +32,19 @@ export const ProductCreateSalesChannelStackedModal = ({
const { getValues, setValue } = form
const { setIsOpen, getIsOpen } = useStackedModal()
const [selection, setSelection] = useState<RowSelectionState>({})
const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>(
{}
)
const [state, setState] = useState<{ id: string; name: string }[]>([])
const { searchParams, raw } = useSalesChannelTableQuery({
const searchParams = hooks.useSalesChannelTableQuery({
pageSize: PAGE_SIZE,
prefix: SC_STACKED_MODAL_ID,
})
const { sales_channels, count, isLoading, isError, error } = useSalesChannels(
searchParams,
{
...searchParams,
placeholderData: keepPreviousData,
}
)
@@ -65,7 +65,7 @@ export const ProductCreateSalesChannelStackedModal = ({
}))
)
setSelection(
setRowSelection(
salesChannels.reduce(
(acc, channel) => ({
...acc,
@@ -77,11 +77,12 @@ export const ProductCreateSalesChannelStackedModal = ({
}
}, [open, getValues])
const updater: OnChangeFn<RowSelectionState> = (fn) => {
const value = typeof fn === "function" ? fn(selection) : fn
const ids = Object.keys(value)
const onRowSelectionChange = (state: DataTableRowSelectionState) => {
const ids = Object.keys(state)
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 }[] = []
@@ -91,10 +92,10 @@ export const ProductCreateSalesChannelStackedModal = ({
}
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]))
})
setSelection(value)
setRowSelection(state)
}
const handleAdd = () => {
@@ -105,23 +106,9 @@ export const ProductCreateSalesChannelStackedModal = ({
setIsOpen(SC_STACKED_MODAL_ID, false)
}
const filters = useSalesChannelTableFilters()
const filters = hooks.useSalesChannelTableFilters()
const columns = useColumns()
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,
})
const emptyState = hooks.useSalesChannelTableEmptyState()
if (isError) {
throw error
@@ -131,22 +118,20 @@ export const ProductCreateSalesChannelStackedModal = ({
<StackedFocusModal.Content className="flex flex-col overflow-hidden">
<StackedFocusModal.Header />
<StackedFocusModal.Body className="flex-1 overflow-hidden">
<_DataTable
table={table}
<DataTable
data={sales_channels}
columns={columns}
pageSize={PAGE_SIZE}
filters={filters}
emptyState={emptyState}
rowCount={count}
pageSize={PAGE_SIZE}
getRowId={(row) => row.id}
rowSelection={{
state: rowSelection,
onRowSelectionChange,
}}
isLoading={isLoading}
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}
/>
</StackedFocusModal.Body>
@@ -166,44 +151,10 @@ export const ProductCreateSalesChannelStackedModal = ({
)
}
const columnHelper =
createColumnHelper<AdminSalesChannelResponse["sales_channel"]>()
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminSalesChannel>()
const useColumns = () => {
const base = useSalesChannelTableColumns()
const base = hooks.useSalesChannelTableColumns()
return useMemo(
() => [
columnHelper.display({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
cell: ({ row }) => {
return (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
)
},
}),
...base,
],
[base]
)
return useMemo(() => [columnHelper.select(), ...base], [base])
}
@@ -18,7 +18,8 @@ import { useTranslation } from "react-i18next"
import { CellContext } from "@tanstack/react-table"
import { useNavigate } from "react-router-dom"
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 {
useDeleteVariantLazy,
useProductVariants,
@@ -144,6 +145,8 @@ const useColumns = (product: HttpTypes.AdminProduct) => {
const { mutateAsync } = useDeleteVariantLazy(product.id)
const prompt = usePrompt()
const dateColumns = useDataTableDateColumns<HttpTypes.AdminProductVariant>()
const handleDelete = useCallback(
async (id: string, title: string) => {
const res = await prompt({
@@ -330,25 +333,28 @@ const useColumns = (product: HttpTypes.AdminProduct) => {
const { text, hasInventoryKit, quantity } = getInventory(row.original)
return (
<div className="flex h-full w-full items-center gap-2 overflow-hidden">
{hasInventoryKit && <Component />}
<span
className={clx("truncate", {
"text-ui-fg-error": !quantity,
})}
title={text}
>
{text}
</span>
</div>
<Tooltip content={text}>
<div className="flex h-full w-full items-center gap-2 overflow-hidden">
{hasInventoryKit && <Component />}
<span
className={clx("truncate", {
"text-ui-fg-error": !quantity,
})}
>
{text}
</span>
</div>
</Tooltip>
)
},
maxSize: 250,
}),
...dateColumns,
columnHelper.action({
actions: getActions,
}),
]
}, [t, optionColumns, getActions, getInventory])
}, [t, optionColumns, dateColumns, getActions, getInventory])
}
const filterHelper =
@@ -1,5 +1,5 @@
import { Button, Checkbox } from "@medusajs/ui"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import { Button, createDataTableColumnHelper } from "@medusajs/ui"
import { RowSelectionState } from "@tanstack/react-table"
import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
@@ -8,17 +8,14 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { HttpTypes } from "@medusajs/types"
import { keepPreviousData } from "@tanstack/react-query"
import { useForm } from "react-hook-form"
import { DataTable } from "../../../../../components/data-table"
import * as hooks from "../../../../../components/data-table/helpers/sales-channels"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { _DataTable } from "../../../../../components/table/data-table"
import { useUpdateProduct } from "../../../../../hooks/api/products"
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 = {
product: HttpTypes.AdminProduct
@@ -29,6 +26,7 @@ const EditSalesChannelsSchema = zod.object({
})
const PAGE_SIZE = 50
const PREFIX = "sc"
export const EditSalesChannelsForm = ({
product,
@@ -62,8 +60,9 @@ export const EditSalesChannelsForm = ({
})
}, [rowSelection, setValue])
const { searchParams, raw } = useSalesChannelTableQuery({
const searchParams = hooks.useSalesChannelTableQuery({
pageSize: PAGE_SIZE,
prefix: PREFIX,
})
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 { 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 handleSubmit = form.handleSubmit(async (data) => {
@@ -123,22 +109,21 @@ export const EditSalesChannelsForm = ({
<div className="flex h-full flex-col overflow-hidden">
<RouteFocusModal.Header />
<RouteFocusModal.Body className="flex-1 overflow-hidden">
<_DataTable
table={table}
<DataTable
data={sales_channels}
columns={columns}
pageSize={PAGE_SIZE}
getRowId={(row) => row.id}
rowCount={count}
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}
rowSelection={{
state: rowSelection,
onRowSelectionChange: setRowSelection,
}}
autoFocusSearch
layout="fill"
emptyState={emptyState}
prefix={PREFIX}
/>
</RouteFocusModal.Body>
<RouteFocusModal.Footer>
@@ -159,43 +144,12 @@ export const EditSalesChannelsForm = ({
}
const columnHelper =
createColumnHelper<HttpTypes.AdminSalesChannelResponse["sales_channel"]>()
createDataTableColumnHelper<
HttpTypes.AdminSalesChannelResponse["sales_channel"]
>()
const useColumns = () => {
const columns = useSalesChannelTableColumns()
const columns = hooks.useSalesChannelTableColumns()
return useMemo(
() => [
columnHelper.display({
id: "select",
header: ({ table }) => {
return (
<Checkbox
checked={
table.getIsSomePageRowsSelected()
? "indeterminate"
: table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
/>
)
},
cell: ({ row }) => {
return (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
)
},
}),
...columns,
],
[columns]
)
return useMemo(() => [columnHelper.select(), ...columns], [columns])
}
@@ -1,32 +1,26 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import {
Button,
Container,
Heading,
Text,
createDataTableColumnHelper,
toast,
usePrompt,
} from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useCallback, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import {
ActionGroup,
ActionMenu,
} from "../../../../components/common/action-menu"
import { _DataTable } from "../../../../components/table/data-table"
import { useNavigate } from "react-router-dom"
import { DataTable } from "../../../../components/data-table"
import * as hooks from "../../../../components/data-table/helpers/sales-channels"
import { useStore } from "../../../../hooks/api"
import {
useDeleteSalesChannel,
useDeleteSalesChannelLazy,
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 SalesChannelWithIsDefault = HttpTypes.AdminSalesChannel & {
is_default?: boolean
}
const PAGE_SIZE = 20
@@ -35,157 +29,130 @@ export const SalesChannelListTable = () => {
const { store } = useStore()
const { raw, searchParams } = useSalesChannelTableQuery({
const searchParams = hooks.useSalesChannelTableQuery({
pageSize: PAGE_SIZE,
})
const {
sales_channels,
count,
isPending: isLoading,
isError,
error,
} = useSalesChannels(searchParams, {
placeholderData: keepPreviousData,
}) as Omit<ReturnType<typeof useSalesChannels>, "sales_channels"> & {
sales_channels: (HttpTypes.AdminSalesChannel & { is_default?: boolean })[]
}
const { sales_channels, count, isPending, isError, error } = useSalesChannels(
searchParams,
{
placeholderData: keepPreviousData,
}
)
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_channel.is_default =
store?.default_sales_channel_id === sales_channel.id
return 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) {
throw error
}
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<div>
<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}
<Container className="p-0">
<DataTable
data={sales_channels_data}
columns={columns}
count={count}
rowCount={count}
getRowId={(row) => row.id}
pageSize={PAGE_SIZE}
filters={filters}
pagination
search
navigateTo={(row) => row.id}
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") },
]}
isLoading={isPending}
emptyState={emptyState}
heading={t("salesChannels.domain")}
subHeading={t("salesChannels.subtitle")}
action={{
label: t("actions.create"),
to: "/settings/sales-channels/create",
}}
rowHref={(row) => `/settings/sales-channels/${row.id}`}
/>
</Container>
)
}
const SalesChannelActions = ({
salesChannel,
}: {
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 columnHelper = createDataTableColumnHelper<
HttpTypes.AdminSalesChannel & { is_default?: boolean }
>()
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(
() => [
...base,
columnHelper.display({
id: "actions",
cell: ({ row }) => {
return <SalesChannelActions salesChannel={row.original} />
columnHelper.action({
actions: (ctx) => {
const disabledTooltip = ctx.row.original.is_default
? 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 { 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 { useDate } from "../../../../../hooks/use-date"
import { useQueryParams } from "../../../../../hooks/use-query-params"
const PAGE_SIZE = 20
@@ -73,7 +73,8 @@ const columnHelper = createDataTableColumnHelper<HttpTypes.AdminUser>()
const useColumns = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const { getFullDate } = useDate()
const dateColumns = useDataTableDateColumns<HttpTypes.AdminUser>()
return useMemo(
() => [
@@ -104,24 +105,7 @@ const useColumns = () => {
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
}),
columnHelper.accessor("created_at", {
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"),
}),
...dateColumns,
columnHelper.action({
actions: [
{
@@ -134,7 +118,7 @@ const useColumns = () => {
],
}),
],
[t, getFullDate, navigate]
[t, navigate, dateColumns]
)
}