feat(ui,dashboard): Add DataTable block (#10024)

**What**
- Adds opinionated DataTable block to `@medusajs/ui` 
- Adds new DataTable to `@medusajs/dashboard` that uses the above mentioned block as the primitive.

The PR also replaces the table on /customer-groups and the variants table on /products/:id with the new DataTable, to provide an example of it's usage. The previous DataTable component has been renamed to `_DataTable` and has been deprecated.

**Note**
This PR has a lot of LOC. 5,346 of these changes are the fr.json file, which wasn't formatted correctly before. When adding the new translations needed for this PR the file was formatted which caused each line to change to have the proper indentation.

Resolves CMRC-333
This commit is contained in:
Kasper Fabricius Kristensen
2025-01-20 14:26:12 +01:00
committed by GitHub
parent c3976a312b
commit 147c0e5a35
130 changed files with 9238 additions and 3884 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/ui": patch
"@medusajs/dashboard": patch
---
feat(ui,dashboard): Add new DataTable block

View File

@@ -87,6 +87,11 @@ module.exports = {
"./packages/admin/admin-bundler/tsconfig.json",
"./packages/admin/admin-vite-plugin/tsconfig.json",
"./packages/design-system/ui/tsconfig.json",
"./packages/design-system/icons/tsconfig.json",
"./packages/design-system/ui-preset/tsconfig.json",
"./packages/design-system/toolbox/tsconfig.json",
"./packages/cli/create-medusa-app/tsconfig.json",
"./packages/cli/medusa-cli/tsconfig.spec.json",
"./packages/cli/oas/medusa-oas-cli/tsconfig.spec.json",
@@ -167,7 +172,10 @@ module.exports = {
},
},
{
files: ["packages/design-system/ui/**/*.{ts,tsx}"],
files: [
"./packages/design-system/ui/**/*.ts",
"./packages/design-system/ui/**/*.tsx",
],
extends: [
"plugin:react/recommended",
"plugin:storybook/recommended",
@@ -196,7 +204,10 @@ module.exports = {
},
},
{
files: ["packages/design-system/icons/**/*.{ts,tsx}"],
files: [
"./packages/design-system/icons/**/*.ts",
"./packages/design-system/icons/**/*.tsx",
],
extends: [
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
@@ -223,8 +234,8 @@ module.exports = {
},
{
files: [
"packages/admin/dashboard/**/*.ts",
"packages/admin/dashboard/**/*.tsx",
"./packages/admin/dashboard/**/*.ts",
"./packages/admin/dashboard/**/*.tsx",
],
plugins: ["unused-imports", "react-refresh"],
extends: [

View File

@@ -7,7 +7,13 @@
"arrowParens": "always",
"overrides": [
{
"files": "./packages/admin-ui/**/*.{js,jsx,ts,tsx}",
"files": "./packages/admin/dashboard/src/**/*.{ts,tsx}",
"options": {
"plugins": ["prettier-plugin-tailwindcss"]
}
},
{
"files": "./packages/design-system/ui/src/**/*.{ts,tsx}",
"options": {
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@@ -30,11 +30,16 @@ export type ActionGroup = {
type ActionMenuProps = PropsWithChildren<{
groups: ActionGroup[]
variant?: "transparent" | "primary"
}>
export const ActionMenu = ({ groups, children }: ActionMenuProps) => {
export const ActionMenu = ({
groups,
variant = "transparent",
children,
}: ActionMenuProps) => {
const inner = children ?? (
<IconButton size="small" variant="transparent">
<IconButton size="small" variant={variant}>
<EllipsisHorizontal />
</IconButton>
)

View File

@@ -0,0 +1,404 @@
import {
Button,
DataTableColumnDef,
DataTableCommand,
DataTableEmptyStateProps,
DataTableFilter,
DataTableFilteringState,
DataTablePaginationState,
DataTableRowSelectionState,
DataTableSortingState,
Heading,
DataTable as Primitive,
useDataTable,
} from "@medusajs/ui"
import React, { ReactNode, useCallback, useState } from "react"
import { useTranslation } from "react-i18next"
import { Link, useNavigate, useSearchParams } from "react-router-dom"
import { useQueryParams } from "../../hooks/use-query-params"
import { ActionMenu } from "../common/action-menu"
type DataTableActionProps = {
label: string
disabled?: boolean
} & (
| {
to: string
}
| {
onClick: () => void
}
)
type DataTableActionMenuActionProps = {
label: string
icon: ReactNode
disabled?: boolean
} & (
| {
to: string
}
| {
onClick: () => void
}
)
type DataTableActionMenuGroupProps = {
actions: DataTableActionMenuActionProps[]
}
type DataTableActionMenuProps = {
groups: DataTableActionMenuGroupProps[]
}
interface DataTableProps<TData> {
data?: TData[]
columns: DataTableColumnDef<TData, any>[]
filters?: DataTableFilter[]
commands?: DataTableCommand[]
action?: DataTableActionProps
actionMenu?: DataTableActionMenuProps
rowCount?: number
getRowId: (row: TData) => string
enablePagination?: boolean
enableSearch?: boolean
autoFocusSearch?: boolean
rowHref?: (row: TData) => string
emptyState?: DataTableEmptyStateProps
heading: string
prefix?: string
pageSize?: number
isLoading?: boolean
rowSelection?: {
state: DataTableRowSelectionState
onRowSelectionChange: (value: DataTableRowSelectionState) => void
}
}
export const DataTable = <TData,>({
data = [],
columns,
filters,
commands,
action,
actionMenu,
getRowId,
rowCount = 0,
enablePagination = true,
enableSearch = true,
autoFocusSearch = false,
rowHref,
heading,
prefix,
pageSize = 10,
emptyState,
rowSelection,
isLoading = false,
}: DataTableProps<TData>) => {
const { t } = useTranslation()
const enableFiltering = filters && filters.length > 0
const enableCommands = commands && commands.length > 0
const enableSorting = columns.some((column) => column.enableSorting)
const filterIds = filters?.map((f) => f.id) ?? []
const prefixedFilterIds = filterIds.map((id) => getQueryParamKey(id, prefix))
const { offset, order, q, ...filterParams } = useQueryParams(
[
...filterIds,
...(enableSorting ? ["order"] : []),
...(enableSearch ? ["q"] : []),
...(enablePagination ? ["offset"] : []),
],
prefix
)
const [_, setSearchParams] = useSearchParams()
const [search, setSearch] = useState<string>(q ?? "")
const handleSearchChange = (value: string) => {
setSearch(value)
setSearchParams((prev) => {
if (value) {
prev.set(getQueryParamKey("q", prefix), value)
} else {
prev.delete(getQueryParamKey("q", prefix))
}
return prev
})
}
const [pagination, setPagination] = useState<DataTablePaginationState>(
offset ? parsePaginationState(offset, pageSize) : { pageIndex: 0, pageSize }
)
const handlePaginationChange = (value: DataTablePaginationState) => {
setPagination(value)
setSearchParams((prev) => {
if (value.pageIndex === 0) {
prev.delete(getQueryParamKey("offset", prefix))
} else {
prev.set(
getQueryParamKey("offset", prefix),
transformPaginationState(value).toString()
)
}
return prev
})
}
const [filtering, setFiltering] = useState<DataTableFilteringState>(
parseFilterState(filterIds, filterParams)
)
const handleFilteringChange = (value: DataTableFilteringState) => {
setFiltering(value)
setSearchParams((prev) => {
Array.from(prev.keys()).forEach((key) => {
if (prefixedFilterIds.includes(key) && !(key in value)) {
prev.delete(key)
}
})
Object.entries(value).forEach(([key, filter]) => {
if (
prefixedFilterIds.includes(getQueryParamKey(key, prefix)) &&
filter
) {
prev.set(getQueryParamKey(key, prefix), JSON.stringify(filter))
}
})
return prev
})
}
const [sorting, setSorting] = useState<DataTableSortingState | null>(
order ? parseSortingState(order) : null
)
const handleSortingChange = (value: DataTableSortingState) => {
setSorting(value)
setSearchParams((prev) => {
if (value) {
const valueToStore = transformSortingState(value)
prev.set(getQueryParamKey("order", prefix), valueToStore)
} else {
prev.delete(getQueryParamKey("order", prefix))
}
return prev
})
}
const { pagination: paginationTranslations, toolbar: toolbarTranslations } =
useDataTableTranslations()
const navigate = useNavigate()
const onRowClick = useCallback(
(event: React.MouseEvent<HTMLTableRowElement, MouseEvent>, row: TData) => {
if (!rowHref) {
return
}
const href = rowHref(row)
if (event.metaKey || event.ctrlKey || event.button === 1) {
window.open(href, "_blank", "noreferrer")
return
}
if (event.shiftKey) {
window.open(href, undefined, "noreferrer")
return
}
navigate(href)
},
[navigate, rowHref]
)
const instance = useDataTable({
data,
columns,
filters,
commands,
rowCount,
getRowId,
onRowClick: rowHref ? onRowClick : undefined,
pagination: enablePagination
? {
state: pagination,
onPaginationChange: handlePaginationChange,
}
: undefined,
filtering: enableFiltering
? {
state: filtering,
onFilteringChange: handleFilteringChange,
}
: undefined,
sorting: enableSorting
? {
state: sorting,
onSortingChange: handleSortingChange,
}
: undefined,
search: enableSearch
? {
state: search,
onSearchChange: handleSearchChange,
}
: undefined,
rowSelection,
isLoading,
})
return (
<Primitive instance={instance}>
<Primitive.Toolbar
className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"
translations={toolbarTranslations}
>
<div className="flex w-full items-center justify-between">
<Heading>{heading}</Heading>
<div className="flex items-center justify-end gap-x-2 md:hidden">
{enableFiltering && (
<Primitive.FilterMenu tooltip={t("filters.filterLabel")} />
)}
<Primitive.SortingMenu tooltip={t("filters.sortLabel")} />
{actionMenu && <ActionMenu variant="primary" {...actionMenu} />}
{action && <DataTableAction {...action} />}
</div>
</div>
<div className="flex w-full items-center gap-2 md:justify-end">
{enableSearch && (
<div className="w-full md:w-auto">
<Primitive.Search
placeholder={t("filters.searchLabel")}
autoFocus={autoFocusSearch}
/>
</div>
)}
<div className="hidden items-center gap-x-2 md:flex">
{enableFiltering && (
<Primitive.FilterMenu tooltip={t("filters.filterLabel")} />
)}
<Primitive.SortingMenu tooltip={t("filters.sortLabel")} />
{actionMenu && <ActionMenu variant="primary" {...actionMenu} />}
{action && <DataTableAction {...action} />}
</div>
</div>
</Primitive.Toolbar>
<Primitive.Table emptyState={emptyState} />
{enablePagination && (
<Primitive.Pagination translations={paginationTranslations} />
)}
{enableCommands && (
<Primitive.CommandBar selectedLabel={(count) => `${count} selected`} />
)}
</Primitive>
)
}
function transformSortingState(value: DataTableSortingState) {
return value.desc ? `-${value.id}` : value.id
}
function parseSortingState(value: string) {
return value.startsWith("-")
? { id: value.slice(1), desc: true }
: { id: value, desc: false }
}
function transformPaginationState(value: DataTablePaginationState) {
return value.pageIndex * value.pageSize
}
function parsePaginationState(value: string, pageSize: number) {
const offset = parseInt(value)
return {
pageIndex: Math.floor(offset / pageSize),
pageSize,
}
}
function parseFilterState(
filterIds: string[],
value: Record<string, string | undefined>
) {
if (!value) {
return {}
}
const filters: DataTableFilteringState = {}
for (const id of filterIds) {
const filterValue = value[id]
if (filterValue) {
filters[id] = {
id,
value: JSON.parse(filterValue),
}
}
}
return filters
}
function getQueryParamKey(key: string, prefix?: string) {
return prefix ? `${prefix}_${key}` : key
}
const useDataTableTranslations = () => {
const { t } = useTranslation()
const paginationTranslations = {
of: t("general.of"),
results: t("general.results"),
pages: t("general.pages"),
prev: t("general.prev"),
next: t("general.next"),
}
const toolbarTranslations = {
clearAll: t("actions.clearAll"),
}
return {
pagination: paginationTranslations,
toolbar: toolbarTranslations,
}
}
const DataTableAction = ({
label,
disabled,
...props
}: DataTableActionProps) => {
const buttonProps = {
size: "small" as const,
disabled: disabled ?? false,
type: "button" as const,
variant: "secondary" as const,
}
if ("to" in props) {
return (
<Button {...buttonProps} asChild>
<Link to={props.to}>{label}</Link>
</Button>
)
}
return (
<Button {...buttonProps} onClick={props.onClick}>
{label}
</Button>
)
}

View File

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

View File

@@ -7,8 +7,8 @@ import { useTranslation } from "react-i18next"
import { useSelectedParams } from "../hooks"
import { useDataTableFilterContext } from "./context"
import { IFilter } from "./types"
import FilterChip from "./filter-chip"
import { IFilter } from "./types"
interface SelectFilterProps extends IFilter {
options: { label: string; value: unknown }[]
@@ -41,7 +41,9 @@ export const SelectFilter = ({
.map((v) => options.find((o) => o.value === v)?.label)
.filter(Boolean) as string[]
const [previousValue, setPreviousValue] = useState<string | string[] | undefined>(labelValues)
const [previousValue, setPreviousValue] = useState<
string | string[] | undefined
>(labelValues)
const handleRemove = () => {
selectedParams.delete()
@@ -84,8 +86,16 @@ export const SelectFilter = ({
}
}
const normalizedValues = labelValues ? (Array.isArray(labelValues) ? labelValues : [labelValues]) : null
const normalizedPrev = previousValue ? (Array.isArray(previousValue) ? previousValue : [previousValue]) : null
const normalizedValues = labelValues
? Array.isArray(labelValues)
? labelValues
: [labelValues]
: null
const normalizedPrev = previousValue
? Array.isArray(previousValue)
? previousValue
: [previousValue]
: null
return (
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>

View File

@@ -176,7 +176,7 @@ export const DataTableRoot = <TData,>({
: undefined,
}}
className={clx({
"bg-ui-bg-base sticky left-0 after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']":
"bg-ui-bg-subtle sticky left-0 after:absolute after:inset-y-0 after:right-0 after:h-full after:w-px after:bg-transparent after:content-['']":
isStickyHeader,
"left-[68px]":
isStickyHeader && hasSelect && !isSelectHeader,

View File

@@ -18,7 +18,10 @@ interface DataTableProps<TData>
// const MemoizedDataTableRoot = memo(DataTableRoot) as typeof DataTableRoot
const MemoizedDataTableQuery = memo(DataTableQuery) as typeof DataTableQuery
export const DataTable = <TData,>({
/**
* @deprecated Use the DataTable component from "/components/data-table" instead
*/
export const _DataTable = <TData,>({
table,
columns,
pagination,

View File

@@ -40,7 +40,7 @@ export const useCustomerGroup = (
}
export const useCustomerGroups = (
query?: Record<string, any>,
query?: HttpTypes.AdminGetCustomerGroupsParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminGetCustomerGroupsParams,
@@ -127,6 +127,29 @@ export const useDeleteCustomerGroup = (
})
}
export const useDeleteCustomerGroupLazy = (
options?: UseMutationOptions<
HttpTypes.AdminCustomerGroupDeleteResponse,
FetchError,
{ id: string }
>
) => {
return useMutation({
mutationFn: ({ id }) => sdk.admin.customerGroup.delete(id),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: customerGroupsQueryKeys.detail(variables.id),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useAddCustomersToGroup = (
id: string,
options?: UseMutationOptions<

View File

@@ -241,6 +241,32 @@ export const useDeleteVariant = (
})
}
export const useDeleteVariantLazy = (
productId: string,
options?: UseMutationOptions<
HttpTypes.AdminProductVariantDeleteResponse,
FetchError,
{ variantId: string }
>
) => {
return useMutation({
mutationFn: ({ variantId }) =>
sdk.admin.product.deleteVariant(productId, variantId),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: variantsQueryKeys.lists() })
queryClient.invalidateQueries({
queryKey: variantsQueryKeys.detail(variables.variantId),
})
queryClient.invalidateQueries({
queryKey: productsQueryKeys.detail(productId),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useProduct = (
id: string,
query?: Record<string, any>,

View File

@@ -0,0 +1,55 @@
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
export const useDateFilterOptions = () => {
const { t } = useTranslation()
const today = useMemo(() => {
const date = new Date()
date.setHours(0, 0, 0, 0)
return date
}, [])
return useMemo(() => {
return [
{
label: t("filters.date.today"),
value: {
$gte: today.toISOString(),
},
},
{
label: t("filters.date.lastSevenDays"),
value: {
$gte: new Date(
today.getTime() - 7 * 24 * 60 * 60 * 1000
).toISOString(), // 7 days ago
},
},
{
label: t("filters.date.lastThirtyDays"),
value: {
$gte: new Date(
today.getTime() - 30 * 24 * 60 * 60 * 1000
).toISOString(), // 30 days ago
},
},
{
label: t("filters.date.lastNinetyDays"),
value: {
$gte: new Date(
today.getTime() - 90 * 24 * 60 * 60 * 1000
).toISOString(), // 90 days ago
},
},
{
label: t("filters.date.lastTwelveMonths"),
value: {
$gte: new Date(
today.getTime() - 365 * 24 * 60 * 60 * 1000
).toISOString(), // 365 days ago
},
},
]
}, [today, t])
}

File diff suppressed because it is too large Load Diff

View File

@@ -316,6 +316,12 @@
"greaterThanLabel": "größer als {{value}}",
"andLabel": "Und"
},
"radio": {
"yes": "Ja",
"no": "Nein",
"true": "Wahr",
"false": "Falsch"
},
"addFilter": "Filter hinzufügen"
},
"errorBoundary": {
@@ -467,7 +473,17 @@
}
},
"deleteWarning": "Sie sind dabei, das Produkt {{title}} zu löschen. Diese Aktion kann nicht rückgängig gemacht werden.",
"variants": "Varianten",
"variants": {
"header": "Varianten",
"empty": {
"heading": "Keine Varianten",
"description": "Es gibt keine Varianten, um angezeigt zu werden."
},
"filtered": {
"heading": "Keine Ergebnisse",
"description": "Keine Varianten stimmen mit den aktuellen Filterkriterien überein."
}
},
"attributes": "Attribute",
"editAttributes": "Attribute bearbeiten",
"editOptions": "Optionen bearbeiten",

View File

@@ -298,6 +298,9 @@
}
},
"filters": {
"sortLabel": "Sort",
"filterLabel": "Filter",
"searchLabel": "Search",
"date": {
"today": "Today",
"lastSevenDays": "Last 7 days",
@@ -306,7 +309,9 @@
"lastTwelveMonths": "Last 12 months",
"custom": "Custom",
"from": "From",
"to": "To"
"to": "To",
"starting": "Starting",
"ending": "Ending"
},
"compare": {
"lessThan": "Less than",
@@ -317,6 +322,18 @@
"greaterThanLabel": "greater than {{value}}",
"andLabel": "and"
},
"sorting": {
"alphabeticallyAsc": "A to Z",
"alphabeticallyDesc": "Z to A",
"dateAsc": "Newest first",
"dateDesc": "Oldest first"
},
"radio": {
"yes": "Yes",
"no": "No",
"true": "True",
"false": "False"
},
"addFilter": "Add filter"
},
"errorBoundary": {
@@ -468,7 +485,17 @@
}
},
"deleteWarning": "You are about to delete the product {{title}}. This action cannot be undone.",
"variants": "Variants",
"variants": {
"header": "Variants",
"empty": {
"heading": "No variants",
"description": "There are no variants to display."
},
"filtered": {
"heading": "No results",
"description": "No variants match the current filter criteria."
}
},
"attributes": "Attributes",
"editAttributes": "Edit Attributes",
"editOptions": "Edit Options",
@@ -891,6 +918,16 @@
"customerGroups": {
"domain": "Customer Groups",
"subtitle": "Organize customers into groups. Groups can have different promotions and prices.",
"list": {
"empty": {
"heading": "No customer groups",
"description": "There are no customer groups to display."
},
"filtered": {
"heading": "No results",
"description": "No customer groups match the current filter criteria."
}
},
"create": {
"header": "Create Customer Group",
"hint": "Create a new customer group to segment your customers.",

View File

@@ -316,6 +316,12 @@
"greaterThanLabel": "mayor que {{value}}",
"andLabel": "y"
},
"radio": {
"yes": "Sí",
"no": "No",
"true": "Verdadero",
"false": "Falso"
},
"addFilter": "Agregar filtro"
},
"errorBoundary": {
@@ -467,7 +473,17 @@
}
},
"deleteWarning": "Estás a punto de eliminar el producto {{title}}. Esta acción no puede deshacerse.",
"variants": "Variantes",
"variants": {
"header": "Variantes",
"empty": {
"heading": "No hay variantes",
"description": "No hay variantes para mostrar."
},
"filtered": {
"heading": "No hay resultados",
"description": "No hay variantes que coincidan con los criterios de filtro actuales."
}
},
"attributes": "Atributos",
"editAttributes": "Editar Atributos",
"editOptions": "Editar Opciones",

File diff suppressed because it is too large Load Diff

View File

@@ -317,6 +317,12 @@
"greaterThanLabel": "maggiore di {{value}}",
"andLabel": "e"
},
"radio": {
"yes": "Sì",
"no": "No",
"true": "Vero",
"false": "Falso"
},
"addFilter": "Aggiungi filtro"
},
"errorBoundary": {
@@ -468,7 +474,17 @@
}
},
"deleteWarning": "Stai per eliminare il prodotto {{title}}. Questa azione non può essere annullata.",
"variants": "Varianti",
"variants": {
"header": "Varianti",
"empty": {
"heading": "Nessuna variante",
"description": "Non ci sono varianti da visualizzare."
},
"filtered": {
"heading": "Nessun risultato",
"description": "Nessuna variante corrisponde ai criteri di filtro correnti."
}
},
"attributes": "Attributi",
"editAttributes": "Modifica Attributi",
"editOptions": "Modifica Opzioni",
@@ -2092,14 +2108,14 @@
"label": "La lista prezzi ha una data di scadenza?",
"hint": "Pianifica la lista prezzi per disattivarsi in futuro."
},
"customerAvailability": {
"header": "Scegli gruppi di clienti",
"label": "Disponibilità cliente",
"hint": "Scegli quali gruppi di clienti la lista prezzi dovrebbe essere applicata.",
"placeholder": "Cerca gruppi di clienti",
"attribute": "Gruppi di clienti"
}
"customerAvailability": {
"header": "Scegli gruppi di clienti",
"label": "Disponibilità cliente",
"hint": "Scegli quali gruppi di clienti la lista prezzi dovrebbe essere applicata.",
"placeholder": "Cerca gruppi di clienti",
"attribute": "Gruppi di clienti"
}
}
},
"profile": {
"domain": "Profilo",
@@ -2771,4 +2787,4 @@
"seconds_one": "Secondo",
"seconds_other": "Secondi"
}
}
}

View File

@@ -317,6 +317,12 @@
"greaterThanLabel": "{{value}}以上",
"andLabel": "かつ"
},
"radio": {
"yes": "はい",
"no": "いいえ",
"true": "真",
"false": "偽"
},
"addFilter": "フィルター追加"
},
"errorBoundary": {
@@ -468,7 +474,17 @@
}
},
"deleteWarning": "商品「{{title}}」を削除しようとしています。この操作は元に戻せません。",
"variants": "バリエーション",
"variants": {
"header": "バリエーション",
"empty": {
"heading": "バリエーションはありません",
"description": "表示するバリエーションはありません。"
},
"filtered": {
"heading": "結果はありません",
"description": "現在のフィルター条件に一致するバリエーションはありません。"
}
},
"attributes": "属性",
"editAttributes": "属性を編集",
"editOptions": "オプションを編集",

View File

@@ -316,6 +316,12 @@
"greaterThanLabel": "więcej niż {{value}}",
"andLabel": "i"
},
"radio": {
"yes": "Tak",
"no": "Nie",
"true": "Prawda",
"false": "Fałsz"
},
"addFilter": "Dodaj filtr"
},
"errorBoundary": {
@@ -467,7 +473,17 @@
}
},
"deleteWarning": "Zamierzasz usunąć produkt {{title}}. Ta akcja nie może zostać cofnięta.",
"variants": "Warianty",
"variants": {
"header": "Warianty",
"empty": {
"heading": "Brak wariantów",
"description": "Nie ma wariantów do wyświetlenia."
},
"filtered": {
"heading": "Brak wariantów",
"description": "Nie ma wariantów, które pasują do aktualnych kryteriów filtrów."
}
},
"attributes": "Atrybuty",
"editAttributes": "Edytuj atrybuty",
"editOptions": "Edytuj opcje",

View File

@@ -316,6 +316,12 @@
"greaterThanLabel": "maior que {{value}}",
"andLabel": "e"
},
"radio": {
"yes": "Sim",
"no": "Não",
"true": "Verdadeiro",
"false": "Falso"
},
"addFilter": "Adicionar filtro"
},
"errorBoundary": {
@@ -467,7 +473,17 @@
}
},
"deleteWarning": "Você está prestes a excluir o produto {{title}}. Esta ação não pode ser desfeita.",
"variants": "Variantes",
"variants": {
"header": "Variantes",
"empty": {
"heading": "Nenhuma variante",
"description": "Não há variantes para exibir."
},
"filtered": {
"heading": "Nenhuma variante",
"description": "Nenhuma variante corresponde aos critérios de filtro atuais."
}
},
"attributes": "Atributos",
"editAttributes": "Editar atributos",
"editOptions": "Editar opções",

View File

@@ -316,6 +316,12 @@
"greaterThanLabel": "มากกว่า {{value}}",
"andLabel": "และ"
},
"radio": {
"yes": "ใช่",
"no": "ไม่",
"true": "จริง",
"false": "เท็จ"
},
"addFilter": "เพิ่มตัวกรอง"
},
"errorBoundary": {
@@ -467,7 +473,17 @@
}
},
"deleteWarning": "คุณกำลังจะลบสินค้า {{title}} การดำเนินการนี้ไม่สามารถยกเลิกได้",
"variants": "ตัวเลือก",
"variants": {
"header": "ตัวเลือก",
"empty": {
"heading": "ไม่มีตัวเลือก",
"description": "ไม่มีตัวเลือกที่จะแสดง"
},
"filtered": {
"heading": "ไม่มีผลลัพธ์",
"description": "ไม่มีตัวเลือกที่ตรงกับเกณฑ์การกรองปัจจุบัน"
}
},
"attributes": "แอตทริบิวต์",
"editAttributes": "แก้ไขแอตทริบิวต์",
"editOptions": "แก้ไขตัวเลือก",

View File

@@ -316,6 +316,12 @@
"greaterThanLabel": "{{value}}'den büyük",
"andLabel": "ve"
},
"radio": {
"yes": "Evet",
"no": "Hayır",
"true": "Doğru",
"false": "Yanlış"
},
"addFilter": "Filtre Ekle"
},
"errorBoundary": {
@@ -467,7 +473,17 @@
}
},
"deleteWarning": "Ürün {{title}}'i silmek üzeresiniz. Bu işlem geri alınamaz.",
"variants": "Varyantlar",
"variants": {
"header": "Varyantlar",
"empty": {
"heading": "Varyant yok",
"description": "Görüntülenecek varyant yok."
},
"filtered": {
"heading": "Sonuç yok",
"description": "Varyantlar mevcut filtrelerle eşleşmiyor."
}
},
"attributes": "Öznitelikler",
"editAttributes": "Öznitelikleri Düzenle",
"editOptions": "Seçenekleri Düzenle",

View File

@@ -317,6 +317,12 @@
"greaterThanLabel": "більше ніж {{value}}",
"andLabel": "і"
},
"radio": {
"yes": "Так",
"no": "немає",
"true": "правда",
"false": "помилковий"
},
"addFilter": "Додати фільтр"
},
"errorBoundary": {
@@ -468,7 +474,17 @@
}
},
"deleteWarning": "Ви збираєтеся видалити продукт {{title}}. Цю дію не можна скасувати.",
"variants": "Варіанти",
"variants": {
"header": "Варіанти",
"empty": {
"heading": "Немає варіантів",
"description": "Немає варіантів для відображення."
},
"filtered": {
"heading": "Немає результатів",
"description": "Немає варіантів, які відповідають поточному критерію фільтрації."
}
},
"attributes": "Атрибути",
"editAttributes": "Редагувати атрибути",
"editOptions": "Редагувати опції",

View File

@@ -6,7 +6,7 @@ import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
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"
@@ -109,7 +109,7 @@ export const ApiKeySalesChannelSection = ({
]}
/>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
filters={filters}

View File

@@ -2,7 +2,7 @@ import { Button, Container, Heading, Text } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useApiKeys } from "../../../../../hooks/api/api-keys"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useApiKeyManagementTableColumns } from "./use-api-key-management-table-columns"
@@ -70,7 +70,7 @@ export const ApiKeyManagementListTable = ({
</Button>
</Link>
</div>
<DataTable
<_DataTable
table={table}
filters={filters}
columns={columns}

View File

@@ -15,7 +15,7 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
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"
@@ -139,7 +139,7 @@ export const ApiKeySalesChannelsForm = ({
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-auto">
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -12,7 +12,7 @@ import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { RouteFocusModal, useRouteModal } from "../../../../components/modals"
import { DataTable } from "../../../../components/table/data-table"
import { _DataTable } from "../../../../components/table/data-table"
import { KeyboundForm } from "../../../../components/utilities/keybound-form"
import { useAddOrRemoveCampaignPromotions } from "../../../../hooks/api/campaigns"
import { usePromotions } from "../../../../hooks/api/promotions"
@@ -124,7 +124,7 @@ export const AddCampaignPromotionsForm = ({
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex size-full flex-col overflow-y-auto">
<DataTable
<_DataTable
table={table}
count={count}
columns={columns}

View File

@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useAddOrRemoveCampaignPromotions } from "../../../../../hooks/api/campaigns"
import { usePromotions } from "../../../../../hooks/api/promotions"
import { usePromotionTableColumns } from "../../../../../hooks/table/columns/use-promotion-table-columns"
@@ -89,7 +89,7 @@ export const CampaignPromotionSection = ({
</Link>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -7,7 +7,7 @@ import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../components/common/action-menu"
import { DataTable } from "../../../../components/table/data-table"
import { _DataTable } from "../../../../components/table/data-table"
import {
useCampaigns,
useDeleteCampaign,
@@ -58,7 +58,7 @@ export const CampaignListTable = () => {
</Link>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -14,7 +14,7 @@ import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useUpdateProductCategoryProducts } from "../../../../../hooks/api/categories"
import { useProducts } from "../../../../../hooks/api/products"
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
@@ -125,7 +125,7 @@ export const CategoryProductSection = ({
]}
/>
</div>
<DataTable
<_DataTable
table={table}
filters={filters}
columns={columns}

View File

@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useProductCategories } from "../../../../../hooks/api/categories"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useDeleteProductCategoryAction } from "../../../common/hooks/use-delete-product-category-action"
@@ -84,7 +84,7 @@ export const CategoryListTable = () => {
</Button>
</div>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -15,7 +15,7 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useUpdateProductCategoryProducts } from "../../../../../hooks/api/categories"
import { useProducts } from "../../../../../hooks/api/products"
@@ -159,7 +159,7 @@ export const EditCategoryProductsForm = ({
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="size-full overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -15,7 +15,7 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals/index.ts"
import { DataTable } from "../../../../../components/table/data-table/data-table.tsx"
import { _DataTable } from "../../../../../components/table/data-table/data-table.tsx"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form/keybound-form.tsx"
import { useUpdateCollectionProducts } from "../../../../../hooks/api/collections.tsx"
import { useProducts } from "../../../../../hooks/api/products.tsx"
@@ -168,7 +168,7 @@ export const AddProductsToCollectionForm = ({
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="size-full overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -6,7 +6,7 @@ import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useUpdateCollectionProducts } from "../../../../../hooks/api/collections"
import { useProducts } from "../../../../../hooks/api/products"
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
@@ -114,7 +114,7 @@ export const CollectionProductSection = ({
]}
/>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
search

View File

@@ -6,7 +6,7 @@ import { HttpTypes } from "@medusajs/types"
import { keepPreviousData } from "@tanstack/react-query"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useCollections } from "../../../../../hooks/api/collections"
import { useCollectionTableColumns } from "../../../../../hooks/table/columns/use-collection-table-columns"
import { useCollectionTableFilters } from "../../../../../hooks/table/filters"
@@ -60,7 +60,7 @@ export const CollectionListTable = () => {
</Button>
</Link>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -15,7 +15,7 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useAddCustomersToGroup } from "../../../../../hooks/api/customer-groups"
import { useCustomers } from "../../../../../hooks/api/customers"
@@ -153,7 +153,7 @@ export const AddCustomersForm = ({
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="size-full overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useRemoveCustomersFromGroup } from "../../../../../hooks/api/customer-groups"
import { useCustomers } from "../../../../../hooks/api/customers"
import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns"
@@ -95,7 +95,7 @@ export const CustomerGroupCustomerSection = ({
</Button>
</Link>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -1,172 +1,249 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import {
Button,
Container,
Heading,
Text,
createDataTableColumnHelper,
createDataTableFilterHelper,
toast,
usePrompt,
} from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { keepPreviousData } from "@tanstack/react-query"
import { useCallback, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { useNavigate } from "react-router-dom"
import { HttpTypes } from "@medusajs/types"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { DataTable } from "../../../../../components/data-table"
import { SingleColumnPage } from "../../../../../components/layout/pages"
import { useDashboardExtension } from "../../../../../extensions"
import {
useCustomerGroups,
useDeleteCustomerGroup,
} from "../../../../../hooks/api/customer-groups"
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
useDeleteCustomerGroupLazy,
} from "../../../../../hooks/api"
import { useDateFilterOptions } from "../../../../../hooks/filters/use-date-filter-options"
import { useDate } from "../../../../../hooks/use-date"
import { useQueryParams } from "../../../../../hooks/use-query-params"
const PAGE_SIZE = 20
const PAGE_SIZE = 10
export const CustomerGroupListTable = () => {
const { t } = useTranslation()
const { getWidgets } = useDashboardExtension()
const { searchParams, raw } = useCustomerGroupTableQuery({
pageSize: PAGE_SIZE,
})
const { customer_groups, count, isLoading, isError, error } =
useCustomerGroups({
...searchParams,
fields: "id,name,customers.id",
})
const { q, order, offset, created_at, updated_at } = useQueryParams([
"q",
"order",
"offset",
"created_at",
"updated_at",
])
const filters = useCustomerGroupTableFilters()
const columns = useColumns()
const filters = useFilters()
const { table } = useDataTable({
data: customer_groups ?? [],
columns,
enablePagination: true,
count,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
const { customer_groups, count, isPending, isError, error } =
useCustomerGroups(
{
q,
order,
offset: offset ? parseInt(offset) : undefined,
limit: PAGE_SIZE,
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
fields: "id,name,created_at,updated_at,customers.id",
},
{
placeholderData: keepPreviousData,
}
)
if (isError) {
throw error
}
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<div>
<Heading level="h2">{t("customerGroups.domain")}</Heading>
<Text className="text-ui-fg-subtle" size="small">
{t("customerGroups.subtitle")}
</Text>
</div>
<Link to="/customer-groups/create">
<Button size="small" variant="secondary">
{t("actions.create")}
</Button>
</Link>
</div>
<DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
count={count}
filters={filters}
search
pagination
navigateTo={(row) => `/customer-groups/${row.original.id}`}
orderBy={[
{ key: "name", label: t("fields.name") },
{ key: "created_at", label: t("fields.createdAt") },
{ key: "updated_at", label: t("fields.updatedAt") },
]}
queryObject={raw}
isLoading={isLoading}
/>
</Container>
<SingleColumnPage
widgets={{
before: getWidgets("customer_group.list.before"),
after: getWidgets("customer_group.list.after"),
}}
>
<Container className="overflow-hidden p-0">
<DataTable
data={customer_groups}
columns={columns}
filters={filters}
heading={t("customerGroups.domain")}
rowCount={count}
getRowId={(row) => row.id}
rowHref={(row) => `/customer-groups/${row.id}`}
action={{
label: t("actions.create"),
to: "/customer-groups/create",
}}
emptyState={{
empty: {
heading: t("customerGroups.list.empty.heading"),
description: t("customerGroups.list.empty.description"),
},
filtered: {
heading: t("customerGroups.list.filtered.heading"),
description: t("customerGroups.list.filtered.description"),
},
}}
pageSize={PAGE_SIZE}
isLoading={isPending}
/>
</Container>
</SingleColumnPage>
)
}
const CustomerGroupRowActions = ({
group,
}: {
group: HttpTypes.AdminCustomerGroup
}) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useDeleteCustomerGroup(group.id)
const handleDelete = async () => {
const res = await prompt({
title: t("customerGroups.delete.title"),
description: t("customerGroups.delete.description", {
name: group.name,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync(undefined, {
onSuccess: () => {
toast.success(
t("customerGroups.delete.successToast", {
name: group.name,
})
)
},
onError: (error) => {
toast.error(error.message)
},
})
}
return (
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
to: `/customer-groups/${group.id}/edit`,
icon: <PencilSquare />,
},
],
},
{
actions: [
{
label: t("actions.delete"),
onClick: handleDelete,
icon: <Trash />,
},
],
},
]}
/>
)
}
const columnHelper = createColumnHelper<HttpTypes.AdminCustomerGroup>()
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminCustomerGroup>()
const useColumns = () => {
const columns = useCustomerGroupTableColumns()
const { t } = useTranslation()
const { getFullDate } = useDate()
const navigate = useNavigate()
const prompt = usePrompt()
return useMemo(
() => [
...columns,
columnHelper.display({
id: "actions",
cell: ({ row }) => <CustomerGroupRowActions group={row.original} />,
}),
],
[columns]
const { mutateAsync: deleteCustomerGroup } = useDeleteCustomerGroupLazy()
const handleDeleteCustomerGroup = useCallback(
async ({ id, name }: { id: string; name: string }) => {
const res = await prompt({
title: t("customerGroups.delete.title"),
description: t("customerGroups.delete.description", {
name,
}),
verificationText: name,
verificationInstruction: t("general.typeToConfirm"),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await deleteCustomerGroup(
{ id },
{
onSuccess: () => {
toast.success(t("customerGroups.delete.successToast", { name }))
},
onError: (e) => {
toast.error(e.message)
},
}
)
},
[t, prompt, deleteCustomerGroup]
)
return useMemo(() => {
return [
columnHelper.accessor("name", {
header: t("fields.name"),
enableSorting: true,
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
}),
columnHelper.accessor("customers", {
header: t("customers.domain"),
cell: ({ row }) => {
return <span>{row.original.customers?.length ?? 0}</span>
},
}),
columnHelper.accessor("created_at", {
header: t("fields.createdAt"),
cell: ({ row }) => {
return (
<span>
{getFullDate({
date: row.original.created_at,
includeTime: true,
})}
</span>
)
},
enableSorting: true,
sortAscLabel: t("filters.sorting.dateAsc"),
sortDescLabel: t("filters.sorting.dateDesc"),
}),
columnHelper.accessor("updated_at", {
header: t("fields.updatedAt"),
cell: ({ row }) => {
return (
<span>
{getFullDate({
date: row.original.updated_at,
includeTime: true,
})}
</span>
)
},
enableSorting: true,
sortAscLabel: t("filters.sorting.dateAsc"),
sortDescLabel: t("filters.sorting.dateDesc"),
}),
columnHelper.action({
actions: [
[
{
icon: <PencilSquare />,
label: t("actions.edit"),
onClick: (row) => {
navigate(`/customer-groups/${row.row.original.id}/edit`)
},
},
],
[
{
icon: <Trash />,
label: t("actions.delete"),
onClick: (row) => {
handleDeleteCustomerGroup({
id: row.row.original.id,
name: row.row.original.name ?? "",
})
},
},
],
],
}),
]
}, [t, navigate, getFullDate, handleDeleteCustomerGroup])
}
const filterHelper = createDataTableFilterHelper<HttpTypes.AdminCustomerGroup>()
const useFilters = () => {
const { t } = useTranslation()
const { getFullDate } = useDate()
const dateFilterOptions = useDateFilterOptions()
return useMemo(() => {
return [
filterHelper.accessor("created_at", {
type: "date",
label: t("fields.createdAt"),
format: "date",
formatDateValue: (date) => getFullDate({ date }),
rangeOptionStartLabel: t("filters.date.starting"),
rangeOptionEndLabel: t("filters.date.ending"),
rangeOptionLabel: t("filters.date.custom"),
options: dateFilterOptions,
}),
filterHelper.accessor("updated_at", {
type: "date",
label: t("fields.updatedAt"),
format: "date",
rangeOptionStartLabel: t("filters.date.starting"),
rangeOptionEndLabel: t("filters.date.ending"),
rangeOptionLabel: t("filters.date.custom"),
formatDateValue: (date) => getFullDate({ date }),
options: dateFilterOptions,
}),
]
}, [t, dateFilterOptions, getFullDate])
}

View File

@@ -15,17 +15,18 @@ import { PencilSquare, Trash } from "@medusajs/icons"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu/index.ts"
import { DataTable } from "../../../../../components/table/data-table/index.ts"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { _DataTable } from "../../../../../components/table/data-table"
import { useBatchCustomerCustomerGroups } from "../../../../../hooks/api"
import {
useCustomerGroups,
useRemoveCustomersFromGroup,
} from "../../../../../hooks/api/customer-groups.tsx"
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns.tsx"
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters.tsx"
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query.tsx"
import { useDataTable } from "../../../../../hooks/use-data-table.tsx"
import { useBatchCustomerCustomerGroups } from "../../../../../hooks/api"
} from "../../../../../hooks/api/customer-groups"
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
type CustomerGroupSectionProps = {
customer: HttpTypes.AdminCustomer
@@ -97,19 +98,23 @@ export const CustomerGroupSection = ({
return
}
try {
await batchCustomerCustomerGroups({ remove: customerGroupIds })
toast.success(
t("customers.groups.removed.success", {
groups: customer_groups!
.filter((cg) => customerGroupIds.includes(cg.id))
.map((cg) => cg?.name),
})
)
} catch (e) {
toast.error(e.message)
}
await batchCustomerCustomerGroups(
{ remove: customerGroupIds },
{
onSuccess: () => {
toast.success(
t("customers.groups.removed.success", {
groups: customer_groups!
.filter((cg) => customerGroupIds.includes(cg.id))
.map((cg) => cg?.name),
})
)
},
onError: (error) => {
toast.error(error.message)
},
}
)
}
if (isError) {
@@ -126,7 +131,7 @@ export const CustomerGroupSection = ({
</Button>
</Link>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
@@ -259,6 +264,6 @@ const useColumns = (customerId: string) => {
),
}),
],
[columns]
[columns, customerId]
)
}

View File

@@ -6,7 +6,7 @@ import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useOrders } from "../../../../../hooks/api/orders"
import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns"
import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters"
@@ -70,7 +70,7 @@ export const CustomerOrderSection = ({
{/* </Button>*/}
{/*</div>*/}
</div>
<DataTable
<_DataTable
columns={columns}
table={table}
pagination

View File

@@ -8,7 +8,7 @@ import { Link } from "react-router-dom"
import { HttpTypes } from "@medusajs/types"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useCustomers } from "../../../../../hooks/api/customers"
import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns"
import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters"
@@ -56,7 +56,7 @@ export const CustomerListTable = () => {
</Button>
</Link>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -15,14 +15,14 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useBatchCustomerCustomerGroups } from "../../../../../hooks/api"
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
import { useCustomerGroupTableQuery } from "../../../../../hooks/table/query/use-customer-group-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useBatchCustomerCustomerGroups } from "../../../../../hooks/api"
type AddCustomerGroupsFormProps = {
customerId: string
@@ -155,7 +155,7 @@ export const AddCustomerGroupsForm = ({
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="size-full overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -1,8 +1,8 @@
import { DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useInventoryItemLevels } from "../../../../../hooks/api/inventory"
import { useLocationLevelTableQuery } from "./use-location-list-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useLocationListTableColumns } from "./use-location-list-table-columns"
import { useLocationLevelTableQuery } from "./use-location-list-table-query"
const PAGE_SIZE = 20
@@ -42,7 +42,7 @@ export const ItemLocationListTable = ({
}
return (
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -1,15 +1,15 @@
import { useMemo } from "react"
import { HttpTypes } from "@medusajs/types"
import { useMemo } from "react"
import { DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useStockLocations } from "../../../../../hooks/api"
import { useReservationItems } from "../../../../../hooks/api/reservations"
import { useDataTable } from "../../../../../hooks/use-data-table"
import {
ExtendedReservationItem,
useReservationTableColumn,
} from "./use-reservation-list-table-columns"
import { useReservationsTableQuery } from "./use-reservation-list-table-query"
import { useStockLocations } from "../../../../../hooks/api"
const PAGE_SIZE = 20
@@ -57,7 +57,7 @@ export const ReservationItemTable = ({
}
return (
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -5,7 +5,7 @@ import { RowSelectionState } from "@tanstack/react-table"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { Link, useNavigate } from "react-router-dom"
import { DataTable } from "../../../../components/table/data-table"
import { _DataTable } from "../../../../components/table/data-table"
import { useInventoryItems } from "../../../../hooks/api/inventory"
import { useDataTable } from "../../../../hooks/use-data-table"
import { INVENTORY_ITEM_IDS_KEY } from "../../common/constants"
@@ -69,7 +69,7 @@ export const InventoryListTable = () => {
<Link to="create">{t("actions.create")}</Link>
</Button>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -15,7 +15,7 @@ import {
StackedFocusModal,
useStackedModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import {
StaticCountry,
@@ -238,7 +238,7 @@ const AreaStackedModal = <TForm extends UseFormReturn<any>>({
</StackedFocusModal.Description>
</StackedFocusModal.Header>
<StackedFocusModal.Body className="flex-1 overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -16,7 +16,7 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useFulfillmentProviders } from "../../../../../hooks/api/fulfillment-providers"
import { useUpdateStockLocationFulfillmentProviders } from "../../../../../hooks/api/stock-locations"
@@ -132,7 +132,7 @@ export const LocationEditFulfillmentProvidersForm = ({
<KeyboundForm onSubmit={handleSubmit} className="flex size-full flex-col">
<RouteFocusModal.Header />
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-auto">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -12,7 +12,7 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
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"
@@ -137,7 +137,7 @@ export const LocationEditSalesChannelsForm = ({
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body>
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -7,7 +7,7 @@ import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
import { getReturnableQuantity } from "../../../../../lib/rma"
@@ -134,7 +134,7 @@ export const AddClaimItemsTable = ({
return (
<div className="flex size-full flex-col overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -2,7 +2,7 @@ import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useVariants } from "../../../../../hooks/api"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useClaimOutboundItemTableColumns } from "./use-claim-outbound-item-table-columns"
@@ -72,7 +72,7 @@ export const AddClaimOutboundItemsTable = ({
return (
<div className="flex size-full flex-col overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -2,7 +2,7 @@ import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useVariants } from "../../../../../hooks/api"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useOrderEditItemsTableColumns } from "./use-order-edit-item-table-columns"
@@ -65,7 +65,7 @@ export const AddOrderEditItemsTable = ({
return (
<div className="flex size-full flex-col overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -3,7 +3,7 @@ import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { getReturnableQuantity } from "../../../../../lib/rma"
import { useExchangeItemTableColumns } from "./use-exchange-item-table-columns"
@@ -102,7 +102,7 @@ export const AddExchangeInboundItemsTable = ({
return (
<div className="flex size-full flex-col overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -2,7 +2,7 @@ import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useVariants } from "../../../../../hooks/api"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useExchangeOutboundItemTableColumns } from "./use-exchange-outbound-item-table-columns"
@@ -72,7 +72,7 @@ export const AddExchangeOutboundItemsTable = ({
return (
<div className="flex size-full flex-col overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -8,7 +8,7 @@ import {
} from "@medusajs/types"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
import { getReturnableQuantity } from "../../../../../lib/rma"
@@ -135,7 +135,7 @@ export const AddReturnItemsTable = ({
return (
<div className="flex size-full flex-col overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -2,7 +2,7 @@ import { Container, Heading } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table/data-table"
import { _DataTable } from "../../../../../components/table/data-table/data-table"
import { useOrders } from "../../../../../hooks/api/orders"
import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns"
import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters"
@@ -49,7 +49,7 @@ export const OrderListTable = () => {
<div className="flex items-center justify-between px-6 py-4">
<Heading>{t("orders.domain")}</Heading>
</div>
<DataTable
<_DataTable
columns={columns}
table={table}
pagination

View File

@@ -10,7 +10,7 @@ import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { StackedDrawer } from "../../../../../components/modals/stacked-drawer"
import { StackedFocusModal } from "../../../../../components/modals/stacked-focus-modal"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useCustomerGroups } from "../../../../../hooks/api/customer-groups"
import { useCustomerGroupTableColumns } from "../../../../../hooks/table/columns/use-customer-group-table-columns"
import { useCustomerGroupTableFilters } from "../../../../../hooks/table/filters/use-customer-group-table-filters"
@@ -119,7 +119,7 @@ export const PriceListCustomerGroupRuleForm = ({
return (
<div className="flex size-full flex-col overflow-hidden">
<Component.Body className="min-h-0 p-0">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -10,7 +10,7 @@ import { useMemo, useState } from "react"
import { UseFormReturn, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useProducts } from "../../../../../hooks/api/products"
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
@@ -117,7 +117,7 @@ export const PriceListProductsForm = ({ form }: PriceListProductsFormProps) => {
return (
<div className="flex size-full flex-col">
<DataTable
<_DataTable
table={table}
columns={columns}
filters={filters}

View File

@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { usePriceListLinkProducts } from "../../../../../hooks/api/price-lists"
import { useProducts } from "../../../../../hooks/api/products"
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
@@ -133,7 +133,7 @@ export const PriceListProductSection = ({
]}
/>
</div>
<DataTable
<_DataTable
table={table}
filters={filters}
columns={columns}

View File

@@ -2,7 +2,7 @@ import { Button, Container, Heading, Text } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { usePriceLists } from "../../../../../hooks/api/price-lists"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { usePricingTableColumns } from "./use-pricing-table-columns"
@@ -53,7 +53,7 @@ export const PriceListListTable = () => {
<Link to="create">{t("actions.create")}</Link>
</Button>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -9,7 +9,7 @@ import {
import { useMemo, useState } from "react"
import { UseFormReturn, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useProducts } from "../../../../../hooks/api/products"
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
@@ -136,7 +136,7 @@ export const PriceListPricesAddProductIdsForm = ({
return (
<div className="flex size-full flex-col">
<DataTable
<_DataTable
table={table}
columns={columns}
filters={filters}

View File

@@ -1,7 +1,7 @@
import { HttpTypes } from "@medusajs/types"
import { Container, Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useProducts } from "../../../../../hooks/api"
import { useProductTableColumns } from "../../../../../hooks/table/columns"
import { useProductTableFilters } from "../../../../../hooks/table/filters"
@@ -51,7 +51,7 @@ export const ProductTagProductSection = ({
<div className="px-6 py-4">
<Heading level="h2">{t("products.domain")}</Heading>
</div>
<DataTable
<_DataTable
table={table}
filters={filters}
queryObject={raw}

View File

@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"
import { Link, useLoaderData } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useProductTags } from "../../../../../hooks/api"
import { useProductTagTableColumns } from "../../../../../hooks/table/columns"
import { useProductTagTableFilters } from "../../../../../hooks/table/filters"
@@ -60,7 +60,7 @@ export const ProductTagListTable = () => {
<Link to="create">{t("actions.create")}</Link>
</Button>
</div>
<DataTable
<_DataTable
table={table}
filters={filters}
queryObject={raw}

View File

@@ -2,7 +2,7 @@ import { HttpTypes } from "@medusajs/types"
import { Container, Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useProducts } from "../../../../../hooks/api/products"
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters"
@@ -48,7 +48,7 @@ export const ProductTypeProductSection = ({
<div className="px-6 py-4">
<Heading level="h2">{t("products.domain")}</Heading>
</div>
<DataTable
<_DataTable
table={table}
filters={filters}
isLoading={isPending}

View File

@@ -6,7 +6,7 @@ import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useProductTypes } from "../../../../../hooks/api/product-types"
import { useProductTypeTableColumns } from "../../../../../hooks/table/columns/use-product-type-table-columns"
import { useProductTypeTableFilters } from "../../../../../hooks/table/filters/use-product-type-table-filters"
@@ -57,7 +57,7 @@ export const ProductTypeListTable = () => {
<Link to="create">{t("actions.create")}</Link>
</Button>
</div>
<DataTable
<_DataTable
table={table}
filters={filters}
isLoading={isLoading}

View File

@@ -1,15 +1,15 @@
import { useTranslation } from "react-i18next"
import { Buildings, Component } from "@medusajs/icons"
import { Container, Heading } from "@medusajs/ui"
import { HttpTypes } from "@medusajs/types"
import { Container, Heading } from "@medusajs/ui"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { LinkButton } from "../../../../../components/common/link-button"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useInventoryTableColumns } from "./use-inventory-table-columns"
import { LinkButton } from "../../../../../components/common/link-button"
const PAGE_SIZE = 20
@@ -62,7 +62,7 @@ export function VariantInventorySection({
</div>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -13,7 +13,7 @@ import {
StackedFocusModal,
useStackedModal,
} from "../../../../../../../components/modals"
import { DataTable } from "../../../../../../../components/table/data-table"
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"
@@ -131,7 +131,7 @@ export const ProductCreateSalesChannelStackedModal = ({
<StackedFocusModal.Content className="flex flex-col overflow-hidden">
<StackedFocusModal.Header />
<StackedFocusModal.Body className="flex-1 overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -1,20 +1,31 @@
import { Buildings, PencilSquare, Plus } from "@medusajs/icons"
import { Buildings, Component, PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { Container, Heading } from "@medusajs/ui"
import {
Badge,
clx,
Container,
createDataTableColumnHelper,
createDataTableCommandHelper,
createDataTableFilterHelper,
DataTableAction,
Tooltip,
usePrompt,
} from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { useCallback, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { RowSelectionState } from "@tanstack/react-table"
import { useState } from "react"
import { CellContext } from "@tanstack/react-table"
import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { useProductVariants } from "../../../../../hooks/api/products"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { DataTable } from "../../../../../components/data-table"
import {
useDeleteVariantLazy,
useProductVariants,
} from "../../../../../hooks/api/products"
import { useDateFilterOptions } from "../../../../../hooks/filters/use-date-filter-options"
import { useDate } from "../../../../../hooks/use-date"
import { useQueryParams } from "../../../../../hooks/use-query-params"
import { PRODUCT_VARIANT_IDS_KEY } from "../../../common/constants"
import { useProductVariantTableColumns } from "./use-variant-table-columns"
import { useProductVariantTableFilters } from "./use-variant-table-filters"
import { useProductVariantTableQuery } from "./use-variant-table-query"
type ProductVariantSectionProps = {
product: HttpTypes.AdminProduct
@@ -26,15 +37,44 @@ export const ProductVariantSection = ({
product,
}: ProductVariantSectionProps) => {
const { t } = useTranslation()
const navigate = useNavigate()
const { searchParams, raw } = useProductVariantTableQuery({
pageSize: PAGE_SIZE,
})
const { variants, count, isLoading, isError, error } = useProductVariants(
const {
q,
order,
offset,
allow_backorder,
manage_inventory,
created_at,
updated_at,
} = useQueryParams([
"q",
"order",
"offset",
"manage_inventory",
"allow_backorder",
"created_at",
"updated_at",
])
const columns = useColumns(product)
const filters = useFilters()
const commands = useCommands()
const { variants, count, isPending, isError, error } = useProductVariants(
product.id,
{
...searchParams,
q,
order,
offset: offset ? parseInt(offset) : undefined,
limit: PAGE_SIZE,
allow_backorder: allow_backorder
? JSON.parse(allow_backorder)
: undefined,
manage_inventory: manage_inventory
? JSON.parse(manage_inventory)
: undefined,
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
fields: "*inventory_items.inventory.location_levels,+inventory_quantity",
},
{
@@ -42,45 +82,40 @@ export const ProductVariantSection = ({
}
)
const [selection, setSelection] = useState<RowSelectionState>({})
const filters = useProductVariantTableFilters()
const columns = useProductVariantTableColumns(product)
const { table } = useDataTable({
data: variants ?? [],
columns,
count,
enablePagination: true,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
enableRowSelection: true,
rowSelection: {
state: selection,
updater: setSelection,
},
meta: {
product,
},
})
if (isError) {
throw error
}
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("products.variants")}</Heading>
<ActionMenu
groups={[
<DataTable
data={variants}
columns={columns}
filters={filters}
rowCount={count}
getRowId={(row) => row.id}
rowHref={(row) => `/products/${product.id}/variants/${row.id}`}
pageSize={PAGE_SIZE}
isLoading={isPending}
heading={t("products.variants.header")}
emptyState={{
empty: {
heading: t("products.variants.empty.heading"),
description: t("products.variants.empty.description"),
},
filtered: {
heading: t("products.variants.filtered.heading"),
description: t("products.variants.filtered.description"),
},
}}
action={{
label: t("actions.create"),
to: `variants/create`,
}}
actionMenu={{
groups: [
{
actions: [
{
label: t("actions.create"),
to: `variants/create`,
icon: <Plus />,
},
{
label: t("products.editPrices"),
to: `prices`,
@@ -93,41 +128,295 @@ export const ProductVariantSection = ({
},
],
},
]}
/>
</div>
<DataTable
table={table}
columns={columns}
filters={filters}
count={count}
pageSize={PAGE_SIZE}
isLoading={isLoading}
orderBy={[
{ key: "title", label: t("fields.title") },
{ key: "created_at", label: t("fields.createdAt") },
{ key: "updated_at", label: t("fields.updatedAt") },
]}
navigateTo={(row) =>
`/products/${row.original.product_id}/variants/${row.id}`
}
pagination
search
queryObject={raw}
commands={[
{
action: async (selection) => {
navigate(
`stock?${PRODUCT_VARIANT_IDS_KEY}=${Object.keys(selection).join(
","
)}`
)
},
label: t("inventory.stock.action"),
shortcut: "i",
},
]}
],
}}
commands={commands}
/>
</Container>
)
}
const columnHelper =
createDataTableColumnHelper<HttpTypes.AdminProductVariant>()
const useColumns = (product: HttpTypes.AdminProduct) => {
const { t } = useTranslation()
const navigate = useNavigate()
const { mutateAsync } = useDeleteVariantLazy(product.id)
const prompt = usePrompt()
const handleDelete = useCallback(
async (id: string, title: string) => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("products.deleteVariantWarning", {
title,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync({ variantId: id })
},
[mutateAsync, prompt, t]
)
const optionColumns = useMemo(() => {
if (!product?.options) {
return []
}
return product.options.map((option) => {
return columnHelper.display({
id: option.id,
header: option.title,
cell: ({ row }) => {
const variantOpt = row.original.options?.find(
(opt) => opt.option_id === option.id
)
if (!variantOpt) {
return <span className="text-ui-fg-muted">-</span>
}
return (
<div className="flex items-center">
<Tooltip content={variantOpt.value}>
<Badge
size="2xsmall"
title={variantOpt.value}
className="inline-flex min-w-[20px] max-w-[140px] items-center justify-center overflow-hidden truncate"
>
{variantOpt.value}
</Badge>
</Tooltip>
</div>
)
},
})
})
}, [product])
const getActions = useCallback(
(ctx: CellContext<HttpTypes.AdminProductVariant, unknown>) => {
const variant = ctx.row.original as HttpTypes.AdminProductVariant & {
inventory_items: { inventory: HttpTypes.AdminInventoryItem }[]
}
const mainActions: DataTableAction<HttpTypes.AdminProductVariant>[] = [
{
icon: <PencilSquare />,
label: t("actions.edit"),
onClick: (row) => {
navigate(`edit-variant?variant_id=${row.row.original.id}`)
},
},
]
const secondaryActions: DataTableAction<HttpTypes.AdminProductVariant>[] =
[
{
icon: <Trash />,
label: t("actions.delete"),
onClick: () => handleDelete(variant.id, variant.title!),
},
]
const inventoryItemsCount = variant.inventory_items?.length || 0
switch (inventoryItemsCount) {
case 0:
break
case 1: {
const inventoryItemLink = `/inventory/${
variant.inventory_items![0].inventory.id
}`
mainActions.push({
label: t("products.variant.inventory.actions.inventoryItems"),
onClick: () => {
navigate(inventoryItemLink)
},
icon: <Buildings />,
})
break
}
default: {
const ids = variant.inventory_items?.map((i) => i.inventory?.id)
if (!ids || ids.length === 0) {
break
}
const inventoryKitLink = `/inventory?${new URLSearchParams({
id: ids.join(","),
}).toString()}`
mainActions.push({
label: t("products.variant.inventory.actions.inventoryKit"),
onClick: () => {
navigate(inventoryKitLink)
},
icon: <Component />,
})
}
}
return [mainActions, secondaryActions]
},
[handleDelete, navigate, t]
)
const getInventory = useCallback(
(variant: HttpTypes.AdminProductVariant) => {
const castVariant = variant as HttpTypes.AdminProductVariant & {
inventory_items: { inventory: HttpTypes.AdminInventoryItem }[]
}
const quantity = variant.inventory_quantity
const inventoryItems = castVariant.inventory_items
?.map((i) => i.inventory)
.filter(Boolean) as HttpTypes.AdminInventoryItem[]
const hasInventoryKit = inventoryItems.length > 1
const locations: Record<string, boolean> = {}
inventoryItems.forEach((i) => {
i.location_levels?.forEach((l) => {
locations[l.id] = true
})
})
const locationCount = Object.keys(locations).length
const text = hasInventoryKit
? t("products.variant.tableItemAvailable", {
availableCount: quantity,
})
: t("products.variant.tableItem", {
availableCount: quantity,
locationCount,
count: locationCount,
})
return { text, hasInventoryKit, quantity }
},
[t]
)
return useMemo(() => {
return [
columnHelper.accessor("title", {
header: t("fields.title"),
enableSorting: true,
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
}),
columnHelper.accessor("sku", {
header: t("fields.sku"),
enableSorting: true,
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
}),
...optionColumns,
columnHelper.display({
id: "inventory",
header: t("fields.inventory"),
cell: ({ row }) => {
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>
)
},
}),
columnHelper.action({
actions: getActions,
}),
]
}, [t, optionColumns, getActions, getInventory])
}
const filterHelper =
createDataTableFilterHelper<HttpTypes.AdminProductVariant>()
const useFilters = () => {
const { t } = useTranslation()
const { getFullDate } = useDate()
const dateFilterOptions = useDateFilterOptions()
return useMemo(() => {
return [
filterHelper.accessor("allow_backorder", {
type: "radio",
label: t("fields.allowBackorder"),
options: [
{ label: t("filters.radio.yes"), value: "true" },
{ label: t("filters.radio.no"), value: "false" },
],
}),
filterHelper.accessor("manage_inventory", {
type: "radio",
label: t("fields.manageInventory"),
options: [
{ label: t("filters.radio.yes"), value: "true" },
{ label: t("filters.radio.no"), value: "false" },
],
}),
filterHelper.accessor("created_at", {
type: "date",
label: t("fields.createdAt"),
format: "date",
formatDateValue: (date) => getFullDate({ date }),
rangeOptionStartLabel: t("filters.date.starting"),
rangeOptionEndLabel: t("filters.date.ending"),
rangeOptionLabel: t("filters.date.custom"),
options: dateFilterOptions,
}),
filterHelper.accessor("updated_at", {
type: "date",
label: t("fields.updatedAt"),
format: "date",
rangeOptionStartLabel: t("filters.date.starting"),
rangeOptionEndLabel: t("filters.date.ending"),
rangeOptionLabel: t("filters.date.custom"),
formatDateValue: (date) => getFullDate({ date }),
options: dateFilterOptions,
}),
]
}, [t, dateFilterOptions, getFullDate])
}
const commandHelper = createDataTableCommandHelper()
const useCommands = () => {
const { t } = useTranslation()
const navigate = useNavigate()
return [
commandHelper.command({
label: t("inventory.stock.action"),
shortcut: "i",
action: async (selection) => {
navigate(
`stock?${PRODUCT_VARIANT_IDS_KEY}=${Object.keys(selection).join(",")}`
)
},
}),
]
}

View File

@@ -1,280 +0,0 @@
import { Buildings, Component, PencilSquare, Trash } from "@medusajs/icons"
import { HttpTypes, InventoryItemDTO } from "@medusajs/types"
import { Badge, Checkbox, clx, usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import {
Action,
ActionMenu,
} from "../../../../../components/common/action-menu"
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
import { useDeleteVariant } from "../../../../../hooks/api/products"
const VariantActions = ({
variant,
product,
}: {
variant: HttpTypes.AdminProductVariant & {
inventory_items: { inventory: InventoryItemDTO }[]
}
product: HttpTypes.AdminProduct
}) => {
const { mutateAsync } = useDeleteVariant(product.id, variant.id)
const { t } = useTranslation()
const prompt = usePrompt()
const inventoryItemsCount = variant.inventory_items?.length || 0
const hasInventoryItem = inventoryItemsCount === 1
const hasInventoryKit = inventoryItemsCount > 1
const handleDelete = async () => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("products.deleteVariantWarning", {
title: variant.title,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync()
}
const [inventoryItemLink, inventoryKitLink] = useMemo(() => {
if (!variant.inventory_items?.length) {
return ["", ""]
}
const itemId = variant.inventory_items![0].inventory.id
const itemLink = `/inventory/${itemId}`
const itemIds = variant.inventory_items!.map((i) => i.inventory.id)
const params = { id: itemIds }
const query = new URLSearchParams(params).toString()
const kitLink = `/inventory?${query}`
return [itemLink, kitLink]
}, [variant.inventory_items])
return (
<ActionMenu
groups={[
{
actions: [
{
label: t("actions.edit"),
to: `edit-variant?variant_id=${variant.id}`,
icon: <PencilSquare />,
},
hasInventoryItem
? {
label: t("products.variant.inventory.actions.inventoryItems"),
to: inventoryItemLink,
icon: <Buildings />,
}
: false,
hasInventoryKit
? {
label: t("products.variant.inventory.actions.inventoryKit"),
to: inventoryKitLink,
icon: <Component />,
}
: false,
].filter(Boolean) as Action[],
},
{
actions: [
{
label: t("actions.delete"),
onClick: handleDelete,
icon: <Trash />,
},
],
},
]}
/>
)
}
const columnHelper = createColumnHelper<HttpTypes.AdminProductVariant>()
export const useProductVariantTableColumns = (
product?: HttpTypes.AdminProduct
) => {
const { t } = useTranslation()
const optionColumns = useMemo(() => {
if (!product?.options) {
return []
}
return product.options.map((option) => {
return columnHelper.display({
id: option.id,
header: () => (
<div className="flex h-full w-full items-center">
<span className="truncate">{option.title}</span>
</div>
),
cell: ({ row }) => {
const variantOpt = row.original.options?.find(
(opt) => opt.option_id === option.id
)
if (!variantOpt) {
return <PlaceholderCell />
}
return (
<div className="flex items-center">
<Badge
size="2xsmall"
title={variantOpt.value}
className="inline-flex min-w-[20px] max-w-[140px] items-center justify-center overflow-hidden truncate"
>
{variantOpt.value}
</Badge>
</div>
)
},
})
})
}, [product])
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.accessor("title", {
header: () => (
<div className="flex h-full w-full items-center">
<span className="truncate">{t("fields.title")}</span>
</div>
),
cell: ({ getValue }) => (
<div className="flex h-full w-full items-center overflow-hidden">
<span className="truncate">{getValue()}</span>
</div>
),
}),
columnHelper.accessor("sku", {
header: () => (
<div className="flex h-full w-full items-center">
<span className="truncate">{t("fields.sku")}</span>
</div>
),
cell: ({ getValue }) => {
const value = getValue()
if (!value) {
return <PlaceholderCell />
}
return (
<div className="flex h-full w-full items-center overflow-hidden">
<span className="truncate">{value}</span>
</div>
)
},
}),
...optionColumns,
columnHelper.accessor("inventory_items", {
header: () => (
<div className="flex h-full w-full items-center">
<span className="truncate">{t("fields.inventory")}</span>
</div>
),
cell: ({ getValue, row }) => {
const variant = row.original
if (!variant.manage_inventory) {
return t("products.variant.inventory.notManaged")
}
const inventory: InventoryItemDTO[] = getValue().map(
(i) => i.inventory
)
const hasInventoryKit = inventory.length > 1
const locations = {}
inventory.forEach((i) => {
i.location_levels.forEach((l) => {
locations[l.id] = true
})
})
const locationCount = Object.keys(locations).length
const text = hasInventoryKit
? t("products.variant.tableItemAvailable", {
availableCount: variant.inventory_quantity,
})
: t("products.variant.tableItem", {
availableCount: variant.inventory_quantity,
locationCount,
count: locationCount,
})
return (
<div className="flex h-full w-full items-center gap-2 overflow-hidden">
{hasInventoryKit && <Component style={{ marginTop: 1 }} />}
<span
className={clx("truncate", {
"text-ui-fg-error": !variant.inventory_quantity,
})}
title={text}
>
{text}
</span>
</div>
)
},
}),
columnHelper.display({
id: "actions",
cell: ({ row, table }) => {
const { product } = table.options.meta as {
product: HttpTypes.AdminProduct
}
return <VariantActions variant={row.original} product={product} />
},
}),
],
[t, optionColumns]
)
}

View File

@@ -1,58 +0,0 @@
import { useTranslation } from "react-i18next"
import { Filter } from "../../../../../components/table/data-table"
export const useProductVariantTableFilters = () => {
const { t } = useTranslation()
let filters: Filter[] = []
const manageInventoryFilter: Filter = {
key: "manage_inventory",
label: t("fields.managedInventory"),
type: "select",
options: [
{
label: t("fields.true"),
value: "true",
},
{
label: t("fields.false"),
value: "false",
},
],
}
const allowBackorderFilter: Filter = {
key: "allow_backorder",
label: t("fields.allowBackorder"),
type: "select",
options: [
{
label: t("fields.true"),
value: "true",
},
{
label: t("fields.false"),
value: "false",
},
],
}
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",
}))
filters = [
...filters,
manageInventoryFilter,
allowBackorderFilter,
...dateFilters,
]
return filters
}

View File

@@ -1,51 +0,0 @@
import { HttpTypes } from "@medusajs/types"
import { useQueryParams } from "../../../../../hooks/use-query-params"
export const useProductVariantTableQuery = ({
pageSize,
prefix,
}: {
pageSize: number
prefix?: string
}) => {
const queryObject = useQueryParams(
[
"offset",
"q",
"manage_inventory",
"allow_backorder",
"order",
"created_at",
"updated_at",
],
prefix
)
const {
offset,
manage_inventory,
allow_backorder,
created_at,
updated_at,
q,
order,
} = queryObject
const searchParams: HttpTypes.AdminProductVariantParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
manage_inventory: manage_inventory
? manage_inventory === "true"
: undefined,
allow_backorder: allow_backorder ? allow_backorder === "true" : undefined,
order,
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
q,
}
return {
searchParams,
raw: queryObject,
}
}

View File

@@ -8,7 +8,7 @@ import { Link, Outlet, useLoaderData, useLocation } from "react-router-dom"
import { HttpTypes } from "@medusajs/types"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import {
useDeleteProduct,
useProducts,
@@ -72,7 +72,7 @@ export const ProductListTable = () => {
</Button>
</div>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -12,7 +12,7 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
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"
@@ -123,7 +123,7 @@ export const EditSalesChannelsForm = ({
<div className="flex h-full flex-col overflow-hidden">
<RouteFocusModal.Header />
<RouteFocusModal.Body className="flex-1 overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -8,7 +8,7 @@ import { Link, Outlet, useLoaderData, useNavigate } from "react-router-dom"
import { keepPreviousData } from "@tanstack/react-query"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import {
useDeletePromotion,
usePromotions,
@@ -62,7 +62,7 @@ export const PromotionListTable = () => {
</Button>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -15,7 +15,7 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useUpdateRegion } from "../../../../../hooks/api/regions"
import { useDataTable } from "../../../../../hooks/use-data-table"
@@ -142,7 +142,7 @@ export const AddCountriesForm = ({ region }: AddCountriesFormProps) => {
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -27,7 +27,7 @@ import {
useRouteModal,
useStackedModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useCreateRegion } from "../../../../../hooks/api/regions"
import { useDataTable } from "../../../../../hooks/use-data-table"
@@ -359,7 +359,7 @@ export const CreateRegionForm = ({
</StackedFocusModal.Title>
</StackedFocusModal.Header>
<StackedFocusModal.Body className="overflow-hidden">
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -9,7 +9,7 @@ import {
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useUpdateRegion } from "../../../../../hooks/api/regions"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useCountries } from "../../../common/hooks/use-countries"
@@ -115,7 +115,7 @@ export const RegionCountrySection = ({ region }: RegionCountrySectionProps) => {
]}
/>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -15,7 +15,7 @@ import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useDeleteRegion, useRegions } from "../../../../../hooks/api/regions"
import { useRegionTableColumns } from "../../../../../hooks/table/columns/use-region-table-columns"
import { useRegionTableFilters } from "../../../../../hooks/table/filters/use-region-table-filters"
@@ -76,7 +76,7 @@ export const RegionListTable = () => {
</Link>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -1,13 +1,13 @@
import { Button, Container, Heading, Text } from "@medusajs/ui"
import { DataTable } from "../../../../../components/table/data-table"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useReservationItems } from "../../../../../hooks/api/reservations"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useReservationTableColumns } from "./use-reservation-table-columns"
import { useReservationTableFilters } from "./use-reservation-table-filters"
import { useReservationTableQuery } from "./use-reservation-table-query"
import { useTranslation } from "react-i18next"
const PAGE_SIZE = 20
@@ -51,7 +51,7 @@ export const ReservationListTable = () => {
<Link to="create">{t("actions.create")}</Link>
</Button>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useReturnReasons } from "../../../../../hooks/api/return-reasons"
import { useReturnReasonTableColumns } from "../../../../../hooks/table/columns"
import { useReturnReasonTableQuery } from "../../../../../hooks/table/query"
@@ -57,7 +57,7 @@ export const ReturnReasonListTable = () => {
<Link to="create">{t("actions.create")}</Link>
</Button>
</div>
<DataTable
<_DataTable
table={table}
queryObject={raw}
count={count}

View File

@@ -12,7 +12,7 @@ import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { RouteFocusModal, useRouteModal } from "../../../../components/modals"
import { DataTable } from "../../../../components/table/data-table"
import { _DataTable } from "../../../../components/table/data-table"
import { KeyboundForm } from "../../../../components/utilities/keybound-form"
import { useProducts } from "../../../../hooks/api/products"
import { useSalesChannelAddProducts } from "../../../../hooks/api/sales-channels"
@@ -134,7 +134,7 @@ export const AddProductsToSalesChannelForm = ({
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex size-full flex-col overflow-y-auto">
<DataTable
<_DataTable
table={table}
count={count}
columns={columns}

View File

@@ -15,7 +15,7 @@ import { Link } from "react-router-dom"
import { HttpTypes, SalesChannelDTO } from "@medusajs/types"
import { keepPreviousData } from "@tanstack/react-query"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useProducts } from "../../../../../hooks/api/products"
import { useSalesChannelRemoveProducts } from "../../../../../hooks/api/sales-channels"
import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns"
@@ -118,7 +118,7 @@ export const SalesChannelProductSection = ({
</Button>
</Link>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -17,7 +17,7 @@ import {
ActionGroup,
ActionMenu,
} from "../../../../components/common/action-menu"
import { DataTable } from "../../../../components/table/data-table"
import { _DataTable } from "../../../../components/table/data-table"
import { useStore } from "../../../../hooks/api"
import {
useDeleteSalesChannel,
@@ -89,7 +89,7 @@ export const SalesChannelListTable = () => {
</Button>
</Link>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -3,7 +3,7 @@ import { Link } from "react-router-dom"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useShippingProfileTableColumns } from "./use-shipping-profile-table-columns"
@@ -55,7 +55,7 @@ export const ShippingProfileListTable = () => {
</Button>
</div>
</div>
<DataTable
<_DataTable
table={table}
pageSize={PAGE_SIZE}
count={count}

View File

@@ -16,7 +16,7 @@ import {
RouteFocusModal,
useRouteModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useCurrencies } from "../../../../../hooks/api/currencies"
import { pricePreferencesQueryKeys } from "../../../../../hooks/api/price-preferences"
@@ -186,7 +186,7 @@ export const AddCurrenciesForm = ({
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
<DataTable
<_DataTable
table={table}
pageSize={PAGE_SIZE}
count={count}

View File

@@ -14,7 +14,7 @@ import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../../components/common/action-menu"
import { DataTable } from "../../../../../../components/table/data-table"
import { _DataTable } from "../../../../../../components/table/data-table"
import { StatusCell } from "../../../../../../components/table/table-cells/common/status-cell"
import { useCurrencies } from "../../../../../../hooks/api/currencies"
import { usePricePreferences } from "../../../../../../hooks/api/price-preferences"
@@ -164,7 +164,7 @@ export const StoreCurrencySection = ({ store }: StoreCurrencySectionProps) => {
]}
/>
</div>
<DataTable
<_DataTable
orderBy={[
{ key: "name", label: t("fields.name") },
{ key: "code", label: t("fields.code") },

View File

@@ -14,7 +14,7 @@ import {
StackedDrawer,
StackedFocusModal,
} from "../../../../../components/modals"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import {
useCollections,
useCustomerGroups,
@@ -201,7 +201,7 @@ const CustomerGroupTable = ({
}
return (
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
@@ -337,7 +337,7 @@ const ProductTable = ({
}
return (
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
@@ -473,7 +473,7 @@ const ProductCollectionTable = ({
}
return (
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
@@ -609,7 +609,7 @@ const ProductTypeTable = ({
}
return (
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}
@@ -745,7 +745,7 @@ const ProductTagTable = ({
}
return (
<DataTable
<_DataTable
table={table}
columns={columns}
pageSize={PAGE_SIZE}

View File

@@ -22,7 +22,7 @@ import * as zod from "zod"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { Form } from "../../../../../components/common/form"
import { RouteFocusModal } from "../../../../../components/modals/index.ts"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form/keybound-form.tsx"
import {
useCreateInvite,
@@ -159,7 +159,7 @@ export const InviteUserForm = () => {
<div className="flex flex-col gap-y-4">
<Heading level="h2">{t("users.pendingInvites")}</Heading>
<Container className="overflow-hidden p-0">
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -2,7 +2,7 @@ import { Button, Container, Heading } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useUsers } from "../../../../../hooks/api/users"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useUserTableColumns } from "./use-user-table-columns"
@@ -49,7 +49,7 @@ export const UserListTable = () => {
<Link to="invite">{t("users.invite")}</Link>
</Button>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -1,7 +1,7 @@
import { Container, Heading, Text } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table"
import { _DataTable } from "../../../../../components/table/data-table"
import { useWorkflowExecutions } from "../../../../../hooks/api/workflow-executions"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useWorkflowExecutionTableColumns } from "./use-workflow-execution-table-columns"
@@ -50,7 +50,7 @@ export const WorkflowExecutionListTable = () => {
</Text>
</div>
</div>
<DataTable
<_DataTable
table={table}
columns={columns}
count={count}

View File

@@ -42,6 +42,7 @@
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@faker-js/faker": "^9.2.0",
"@medusajs/ui-preset": "^2.2.0",
"@storybook/addon-essentials": "^8.3.5",
"@storybook/addon-interactions": "^8.3.5",
@@ -96,6 +97,7 @@
"@radix-ui/react-switch": "1.1.0",
"@radix-ui/react-tabs": "1.1.0",
"@radix-ui/react-tooltip": "1.1.2",
"@tanstack/react-table": "8.20.5",
"clsx": "^1.2.1",
"copy-to-clipboard": "^3.3.3",
"cva": "1.0.0-beta.1",

View File

@@ -0,0 +1,83 @@
"use client"
import * as React from "react"
import { EllipsisHorizontal } from "@medusajs/icons"
import { CellContext } from "@tanstack/react-table"
import { DropdownMenu } from "../../../components/dropdown-menu"
import { IconButton } from "../../../components/icon-button"
import { DataTableActionColumnDefMeta } from "../types"
interface DataTableActionCellProps<TData> {
ctx: CellContext<TData, unknown>
}
const DataTableActionCell = <TData,>({
ctx,
}: DataTableActionCellProps<TData>) => {
const meta = ctx.column.columnDef.meta as
| DataTableActionColumnDefMeta<TData>
| undefined
const actions = meta?.___actions
if (!actions) {
return null
}
const resolvedActions = typeof actions === "function" ? actions(ctx) : actions
if (!Array.isArray(resolvedActions)) {
return null
}
return (
<DropdownMenu>
<DropdownMenu.Trigger asChild className="ml-1">
<IconButton size="small" variant="transparent">
<EllipsisHorizontal />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content side="bottom">
{resolvedActions.map((actionOrGroup, idx) => {
const isArray = Array.isArray(actionOrGroup)
const isLast = idx === resolvedActions.length - 1
return isArray ? (
<React.Fragment key={idx}>
{actionOrGroup.map((action) => (
<DropdownMenu.Item
key={action.label}
onClick={(e) => {
e.stopPropagation()
action.onClick(ctx)
}}
className="[&>svg]:text-ui-fg-subtle flex items-center gap-2"
>
{action.icon}
{action.label}
</DropdownMenu.Item>
))}
{!isLast && <DropdownMenu.Separator />}
</React.Fragment>
) : (
<DropdownMenu.Item
key={actionOrGroup.label}
onClick={(e) => {
e.stopPropagation()
actionOrGroup.onClick(ctx)
}}
className="[&>svg]:text-ui-fg-subtle flex items-center gap-2"
>
{actionOrGroup.icon}
{actionOrGroup.label}
</DropdownMenu.Item>
)
})}
</DropdownMenu.Content>
</DropdownMenu>
)
}
export { DataTableActionCell }
export type { DataTableActionCellProps }

View File

@@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import { useDataTableContext } from "@/blocks/data-table/context/use-data-table-context"
import { CommandBar } from "@/components/command-bar"
interface DataTableCommandBarProps {
selectedLabel?: ((count: number) => string) | string
}
const DataTableCommandBar = (props: DataTableCommandBarProps) => {
const { instance } = useDataTableContext()
const commands = instance.getCommands()
const rowSelection = instance.getRowSelection()
const count = Object.keys(rowSelection || []).length
const open = commands && commands.length > 0 && count > 0
function getSelectedLabel(count: number) {
if (typeof props.selectedLabel === "function") {
return props.selectedLabel(count)
}
return props.selectedLabel
}
if (!commands || commands.length === 0) {
return null
}
return (
<CommandBar open={open}>
<CommandBar.Bar>
{props.selectedLabel && (
<React.Fragment>
<CommandBar.Value>{getSelectedLabel(count)}</CommandBar.Value>
<CommandBar.Seperator />
</React.Fragment>
)}
{commands.map((command, idx) => (
<React.Fragment key={idx}>
<CommandBar.Command
key={command.label}
action={() => command.action(rowSelection)}
label={command.label}
shortcut={command.shortcut}
/>
{idx < commands.length - 1 && <CommandBar.Seperator />}
</React.Fragment>
))}
</CommandBar.Bar>
</CommandBar>
)
}
export { DataTableCommandBar }
export type { DataTableCommandBarProps }

View File

@@ -0,0 +1,72 @@
"use client"
import * as React from "react"
import { DataTableFilter } from "@/blocks/data-table/components/data-table-filter"
import { useDataTableContext } from "@/blocks/data-table/context/use-data-table-context"
import { Button } from "@/components/button"
import { Skeleton } from "@/components/skeleton"
interface DataTableFilterBarProps {
clearAllFiltersLabel?: string
}
const DataTableFilterBar = ({
clearAllFiltersLabel = "Clear all",
}: DataTableFilterBarProps) => {
const { instance } = useDataTableContext()
const filterState = instance.getFiltering()
const clearFilters = React.useCallback(() => {
instance.clearFilters()
}, [instance])
const filterCount = Object.keys(filterState).length
if (filterCount === 0) {
return null
}
if (instance.showSkeleton) {
return <DataTableFilterBarSkeleton filterCount={filterCount} />
}
return (
<div className="bg-ui-bg-subtle flex w-full flex-nowrap items-center gap-2 overflow-x-auto border-t px-6 py-2 md:flex-wrap">
{Object.entries(filterState).map(([id, filter]) => (
<DataTableFilter key={id} id={id} filter={filter} />
))}
{filterCount > 0 ? (
<Button
variant="transparent"
size="small"
className="text-ui-fg-muted hover:text-ui-fg-subtle flex-shrink-0 whitespace-nowrap"
type="button"
onClick={clearFilters}
>
{clearAllFiltersLabel}
</Button>
) : null}
</div>
)
}
const DataTableFilterBarSkeleton = ({
filterCount,
}: {
filterCount: number
}) => {
return (
<div className="bg-ui-bg-subtle flex w-full flex-nowrap items-center gap-2 overflow-x-auto border-t px-6 py-2 md:flex-wrap">
{Array.from({ length: filterCount }).map((_, index) => (
<Skeleton key={index} className="h-7 w-[180px]" />
))}
{filterCount > 0 ? <Skeleton className="h-7 w-[66px]" /> : null}
</div>
)
}
export { DataTableFilterBar }
export type { DataTableFilterBarProps }

View File

@@ -0,0 +1,66 @@
import * as React from "react"
import { useDataTableContext } from "@/blocks/data-table/context/use-data-table-context"
import { DropdownMenu } from "@/components/dropdown-menu"
import { IconButton } from "@/components/icon-button"
import { Skeleton } from "@/components/skeleton"
import { Tooltip } from "@/components/tooltip"
import { Funnel } from "@medusajs/icons"
interface DataTableFilterMenuProps {
tooltip?: string
}
const DataTableFilterMenu = (props: DataTableFilterMenuProps) => {
const { instance } = useDataTableContext()
const enabledFilters = Object.keys(instance.getFiltering())
const filterOptions = instance
.getFilters()
.filter((filter) => !enabledFilters.includes(filter.id))
if (!enabledFilters.length && !filterOptions.length) {
throw new Error(
"DataTable.FilterMenu was rendered but there are no filters to apply. Make sure to pass filters to 'useDataTable'"
)
}
const Wrapper = props.tooltip ? Tooltip : React.Fragment
if (instance.showSkeleton) {
return <DataTableFilterMenuSkeleton />
}
return (
<DropdownMenu>
<Wrapper content={props.tooltip} hidden={filterOptions.length === 0}>
<DropdownMenu.Trigger asChild disabled={filterOptions.length === 0}>
<IconButton size="small">
<Funnel />
</IconButton>
</DropdownMenu.Trigger>
</Wrapper>
<DropdownMenu.Content side="bottom">
{filterOptions.map((filter) => (
<DropdownMenu.Item
key={filter.id}
onClick={() => {
instance.addFilter({ id: filter.id, value: undefined })
}}
>
{filter.label}
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu>
)
}
const DataTableFilterMenuSkeleton = () => {
return <Skeleton className="size-7" />
}
export { DataTableFilterMenu }
export type { DataTableFilterMenuProps }

View File

@@ -0,0 +1,616 @@
"use client"
import { CheckMini, EllipseMiniSolid, XMark } from "@medusajs/icons"
import * as React from "react"
import { useDataTableContext } from "@/blocks/data-table/context/use-data-table-context"
import type {
DataTableDateComparisonOperator,
DataTableDateFilterProps,
DataTableFilterOption,
} from "@/blocks/data-table/types"
import { isDateComparisonOperator } from "@/blocks/data-table/utils/is-date-comparison-operator"
import { DatePicker } from "@/components/date-picker"
import { Label } from "@/components/label"
import { Popover } from "@/components/popover"
import { clx } from "@/utils/clx"
interface DataTableFilterProps {
id: string
filter: unknown
}
const DEFAULT_FORMAT_DATE_VALUE = (d: Date) =>
d.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})
const DEFAULT_RANGE_OPTION_LABEL = "Custom"
const DEFAULT_RANGE_OPTION_START_LABEL = "Starting"
const DEFAULT_RANGE_OPTION_END_LABEL = "Ending"
const DataTableFilter = ({ id, filter }: DataTableFilterProps) => {
const { instance } = useDataTableContext()
const [open, setOpen] = React.useState(filter === undefined)
const [isCustom, setIsCustom] = React.useState(false)
const onOpenChange = React.useCallback(
(open: boolean) => {
if (
!open &&
(!filter || (Array.isArray(filter) && filter.length === 0))
) {
instance.removeFilter(id)
}
setOpen(open)
},
[instance, id, filter]
)
const removeFilter = React.useCallback(() => {
instance.removeFilter(id)
}, [instance, id])
const meta = instance.getFilterMeta(id)
const { type, options, label, ...rest } = meta ?? {}
const { displayValue, isCustomRange } = React.useMemo(() => {
let displayValue: string | null = null
let isCustomRange = false
if (typeof filter === "string") {
displayValue = options?.find((o) => o.value === filter)?.label ?? null
}
if (Array.isArray(filter)) {
displayValue =
filter
.map((v) => options?.find((o) => o.value === v)?.label)
.join(", ") ?? null
}
if (isDateComparisonOperator(filter)) {
displayValue =
options?.find((o) => {
if (!isDateComparisonOperator(o.value)) {
return false
}
return (
!isCustom &&
(filter.$gte === o.value.$gte || (!filter.$gte && !o.value.$gte)) &&
(filter.$lte === o.value.$lte || (!filter.$lte && !o.value.$lte)) &&
(filter.$gt === o.value.$gt || (!filter.$gt && !o.value.$gt)) &&
(filter.$lt === o.value.$lt || (!filter.$lt && !o.value.$lt))
)
})?.label ?? null
if (!displayValue && isDateFilterProps(meta)) {
const formatDateValue = meta.formatDateValue
? meta.formatDateValue
: DEFAULT_FORMAT_DATE_VALUE
if (filter.$gte && !filter.$lte) {
isCustomRange = true
displayValue = `${
meta.rangeOptionStartLabel || DEFAULT_RANGE_OPTION_START_LABEL
} ${formatDateValue(new Date(filter.$gte))}`
}
if (filter.$lte && !filter.$gte) {
isCustomRange = true
displayValue = `${
meta.rangeOptionEndLabel || DEFAULT_RANGE_OPTION_END_LABEL
} ${formatDateValue(new Date(filter.$lte))}`
}
if (filter.$gte && filter.$lte) {
isCustomRange = true
displayValue = `${formatDateValue(
new Date(filter.$gte)
)} - ${formatDateValue(new Date(filter.$lte))}`
}
}
}
return { displayValue, isCustomRange }
}, [filter, options])
React.useEffect(() => {
if (isCustomRange && !isCustom) {
setIsCustom(true)
}
}, [isCustomRange, isCustom])
if (!meta) {
return null
}
return (
<Popover open={open} onOpenChange={onOpenChange} modal>
<Popover.Anchor asChild>
<div
className={clx(
"bg-ui-bg-component flex flex-shrink-0 items-center overflow-hidden rounded-md",
"[&>*]:txt-compact-small-plus [&>*]:flex [&>*]:items-center [&>*]:justify-center",
{
"shadow-borders-base divide-x": displayValue,
"border border-dashed": !displayValue,
}
)}
>
{displayValue && (
<div className="text-ui-fg-muted whitespace-nowrap px-2 py-1">
{label || id}
</div>
)}
<Popover.Trigger
className={clx(
"text-ui-fg-subtle hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed transition-fg whitespace-nowrap px-2 py-1 outline-none",
{
"text-ui-fg-muted": !displayValue,
}
)}
>
{displayValue || label || id}
</Popover.Trigger>
{displayValue && (
<button
type="button"
className="text-ui-fg-muted hover:bg-ui-bg-base-hover active:bg-ui-bg-base-pressed transition-fg size-7 outline-none"
onClick={removeFilter}
>
<XMark />
</button>
)}
</div>
</Popover.Anchor>
<Popover.Content
align="start"
className="bg-ui-bg-component p-0 outline-none"
>
{(() => {
switch (type) {
case "select":
return (
<DataTableFilterSelectContent
id={id}
filter={filter as string[] | undefined}
options={options as DataTableFilterOption<string>[]}
/>
)
case "radio":
return (
<DataTableFilterRadioContent
id={id}
filter={filter}
options={options as DataTableFilterOption<string>[]}
/>
)
case "date":
return (
<DataTableFilterDateContent
id={id}
filter={filter}
options={
options as DataTableFilterOption<DataTableDateComparisonOperator>[]
}
isCustom={isCustom}
setIsCustom={setIsCustom}
{...rest}
/>
)
default:
return null
}
})()}
</Popover.Content>
</Popover>
)
}
type DataTableFilterDateContentProps = {
id: string
filter: unknown
options: DataTableFilterOption<DataTableDateComparisonOperator>[]
isCustom: boolean
setIsCustom: (isCustom: boolean) => void
} & Pick<
DataTableDateFilterProps,
| "format"
| "rangeOptionLabel"
| "disableRangeOption"
| "rangeOptionStartLabel"
| "rangeOptionEndLabel"
>
const DataTableFilterDateContent = ({
id,
filter,
options,
format = "date",
rangeOptionLabel = DEFAULT_RANGE_OPTION_LABEL,
rangeOptionStartLabel = DEFAULT_RANGE_OPTION_START_LABEL,
rangeOptionEndLabel = DEFAULT_RANGE_OPTION_END_LABEL,
disableRangeOption = false,
isCustom,
setIsCustom,
}: DataTableFilterDateContentProps) => {
const currentValue = filter as DataTableDateComparisonOperator | undefined
const { instance } = useDataTableContext()
const selectedValue = React.useMemo(() => {
if (!currentValue || isCustom) {
return undefined
}
return JSON.stringify(currentValue)
}, [currentValue, isCustom])
const onValueChange = React.useCallback(
(valueStr: string) => {
setIsCustom(false)
const value = JSON.parse(valueStr) as DataTableDateComparisonOperator
instance.updateFilter({ id, value })
},
[instance, id]
)
const onSelectCustom = React.useCallback(() => {
setIsCustom(true)
instance.updateFilter({ id, value: undefined })
}, [instance, id])
const onCustomValueChange = React.useCallback(
(input: "$gte" | "$lte", value: Date | null) => {
const newCurrentValue = { ...currentValue }
newCurrentValue[input] = value ? value.toISOString() : undefined
instance.updateFilter({ id, value: newCurrentValue })
},
[instance, id]
)
const { focusedIndex, setFocusedIndex } = useKeyboardNavigation(
options,
(index) => {
if (index === options.length && !disableRangeOption) {
onSelectCustom()
} else {
onValueChange(JSON.stringify(options[index].value))
}
},
disableRangeOption ? 0 : 1
)
const granularity = format === "date-time" ? "minute" : "day"
const maxDate = currentValue?.$lte
? granularity === "minute"
? new Date(currentValue.$lte)
: new Date(new Date(currentValue.$lte).setHours(23, 59, 59, 999))
: undefined
const minDate = currentValue?.$gte
? granularity === "minute"
? new Date(currentValue.$gte)
: new Date(new Date(currentValue.$gte).setHours(0, 0, 0, 0))
: undefined
const initialFocusedIndex = isCustom ? options.length : 0
const onListFocus = React.useCallback(() => {
if (focusedIndex === -1) {
setFocusedIndex(initialFocusedIndex)
}
}, [focusedIndex, initialFocusedIndex])
return (
<React.Fragment>
<div
className="flex flex-col p-1 outline-none"
tabIndex={0}
role="list"
onFocus={onListFocus}
autoFocus
>
{options.map((option, idx) => {
const value = JSON.stringify(option.value)
const isSelected = selectedValue === value
return (
<OptionButton
key={idx}
index={idx}
option={option}
isSelected={isSelected}
isFocused={focusedIndex === idx}
onClick={() => onValueChange(value)}
onMouseEvent={setFocusedIndex}
icon={EllipseMiniSolid}
/>
)
})}
{!disableRangeOption && (
<OptionButton
index={options.length}
option={{
label: rangeOptionLabel,
value: "__custom",
}}
icon={EllipseMiniSolid}
isSelected={isCustom}
isFocused={focusedIndex === options.length}
onClick={onSelectCustom}
onMouseEvent={setFocusedIndex}
/>
)}
</div>
{!disableRangeOption && isCustom && (
<React.Fragment>
<div className="flex flex-col py-[3px]">
<div className="bg-ui-border-menu-top h-px w-full" />
<div className="bg-ui-border-menu-bot h-px w-full" />
</div>
<div className="flex flex-col gap-2 px-2 pb-3 pt-1">
<div className="flex flex-col gap-1">
<Label id="custom-start-date-label" size="xsmall" weight="plus">
{rangeOptionStartLabel}
</Label>
<DatePicker
aria-labelledby="custom-start-date-label"
granularity={granularity}
maxValue={maxDate}
value={currentValue?.$gte ? new Date(currentValue.$gte) : null}
onChange={(value) => onCustomValueChange("$gte", value)}
/>
</div>
<div className="flex flex-col gap-1">
<Label id="custom-end-date-label" size="xsmall" weight="plus">
{rangeOptionEndLabel}
</Label>
<DatePicker
aria-labelledby="custom-end-date-label"
granularity={granularity}
minValue={minDate}
value={currentValue?.$lte ? new Date(currentValue.$lte) : null}
onChange={(value) => onCustomValueChange("$lte", value)}
/>
</div>
</div>
</React.Fragment>
)}
</React.Fragment>
)
}
type DataTableFilterSelectContentProps = {
id: string
filter?: string[]
options: DataTableFilterOption<string>[]
}
const DataTableFilterSelectContent = ({
id,
filter = [],
options,
}: DataTableFilterSelectContentProps) => {
const { instance } = useDataTableContext()
const onValueChange = React.useCallback(
(value: string) => {
if (filter?.includes(value)) {
const newValues = filter?.filter((v) => v !== value)
instance.updateFilter({
id,
value: newValues,
})
} else {
instance.updateFilter({
id,
value: [...(filter ?? []), value],
})
}
},
[instance, id, filter]
)
const { focusedIndex, setFocusedIndex } = useKeyboardNavigation(
options,
(index) => onValueChange(options[index].value)
)
const onListFocus = React.useCallback(() => {
if (focusedIndex === -1) {
setFocusedIndex(0)
}
}, [focusedIndex])
return (
<div
className="flex flex-col p-1 outline-none"
role="list"
tabIndex={0}
onFocus={onListFocus}
autoFocus
>
{options.map((option, idx) => {
const isSelected = !!filter?.includes(option.value)
return (
<OptionButton
key={idx}
index={idx}
option={option}
isSelected={isSelected}
isFocused={focusedIndex === idx}
onClick={() => onValueChange(option.value)}
onMouseEvent={setFocusedIndex}
icon={CheckMini}
/>
)
})}
</div>
)
}
type DataTableFilterRadioContentProps = {
id: string
filter: unknown
options: DataTableFilterOption<string>[]
}
const DataTableFilterRadioContent = ({
id,
filter,
options,
}: DataTableFilterRadioContentProps) => {
const { instance } = useDataTableContext()
const onValueChange = React.useCallback(
(value: string) => {
instance.updateFilter({ id, value })
},
[instance, id]
)
const { focusedIndex, setFocusedIndex } = useKeyboardNavigation(
options,
(index) => onValueChange(options[index].value)
)
const onListFocus = React.useCallback(() => {
if (focusedIndex === -1) {
setFocusedIndex(0)
}
}, [focusedIndex])
return (
<div
className="flex flex-col p-1 outline-none"
role="list"
tabIndex={0}
onFocus={onListFocus}
autoFocus
>
{options.map((option, idx) => {
const isSelected = filter === option.value
return (
<OptionButton
key={idx}
index={idx}
option={option}
isSelected={isSelected}
isFocused={focusedIndex === idx}
onClick={() => onValueChange(option.value)}
onMouseEvent={setFocusedIndex}
icon={EllipseMiniSolid}
/>
)
})}
</div>
)
}
function isDateFilterProps(props?: unknown | null): props is DataTableDateFilterProps {
if (!props) {
return false
}
return (props as DataTableDateFilterProps).type === "date"
}
type OptionButtonProps = {
index: number
option: DataTableFilterOption<string | DataTableDateComparisonOperator>
isSelected: boolean
isFocused: boolean
onClick: () => void
onMouseEvent: (idx: number) => void
icon: React.ElementType
}
const OptionButton = React.memo(
({
index,
option,
isSelected,
isFocused,
onClick,
onMouseEvent,
icon: Icon,
}: OptionButtonProps) => (
<button
type="button"
role="listitem"
className={clx(
"bg-ui-bg-component txt-compact-small transition-fg flex items-center gap-2 rounded px-2 py-1 outline-none",
{ "bg-ui-bg-component-hover": isFocused }
)}
onClick={onClick}
onMouseEnter={() => onMouseEvent(index)}
onMouseLeave={() => onMouseEvent(-1)}
tabIndex={-1}
>
<div className="flex size-[15px] items-center justify-center">
{isSelected && <Icon />}
</div>
<span>{option.label}</span>
</button>
)
)
function useKeyboardNavigation(
options: unknown[],
onSelect: (index: number) => void,
extraItems: number = 0
) {
const [focusedIndex, setFocusedIndex] = React.useState(-1)
const onKeyDown = React.useCallback(
(e: KeyboardEvent) => {
const totalLength = options.length + extraItems
if ((document.activeElement as HTMLElement).contentEditable === "true") {
return
}
switch (e.key) {
case "ArrowDown":
e.preventDefault()
setFocusedIndex((prev) => (prev < totalLength - 1 ? prev + 1 : prev))
break
case "ArrowUp":
e.preventDefault()
setFocusedIndex((prev) => (prev > 0 ? prev - 1 : prev))
break
case " ":
case "Enter":
e.preventDefault()
if (focusedIndex >= 0) {
onSelect(focusedIndex)
}
break
}
},
[options.length, extraItems, focusedIndex, onSelect]
)
React.useEffect(() => {
window.addEventListener("keydown", onKeyDown)
return () => {
window.removeEventListener("keydown", onKeyDown)
}
}, [onKeyDown])
return { focusedIndex, setFocusedIndex }
}
export { DataTableFilter }
export type { DataTableFilterProps }

View File

@@ -0,0 +1,59 @@
"use client"
import * as React from "react"
import { useDataTableContext } from "@/blocks/data-table/context/use-data-table-context"
import { Skeleton } from "@/components/skeleton"
import { Table } from "@/components/table"
interface DataTablePaginationProps {
translations?: React.ComponentProps<typeof Table.Pagination>["translations"]
}
const DataTablePagination = (props: DataTablePaginationProps) => {
const { instance } = useDataTableContext()
if (!instance.enablePagination) {
throw new Error(
"DataTable.Pagination was rendered but pagination is not enabled. Make sure to pass pagination to 'useDataTable'"
)
}
if (instance.showSkeleton) {
return <DataTablePaginationSkeleton />
}
return (
<Table.Pagination
translations={props.translations}
className="flex-shrink-0"
canNextPage={instance.getCanNextPage()}
canPreviousPage={instance.getCanPreviousPage()}
pageCount={instance.getPageCount()}
count={instance.rowCount}
nextPage={instance.nextPage}
previousPage={instance.previousPage}
pageIndex={instance.pageIndex}
pageSize={instance.pageSize}
/>
)
}
const DataTablePaginationSkeleton = () => {
return (
<div>
<div className="flex items-center justify-between p-4">
<Skeleton className="h-7 w-[138px]" />
<div className="flex items-center gap-x-2">
<Skeleton className="h-7 w-24" />
<Skeleton className="h-7 w-11" />
<Skeleton className="h-7 w-11" />
</div>
</div>
</div>
)
}
export { DataTablePagination }
export type { DataTablePaginationProps }

View File

@@ -0,0 +1,53 @@
"use client"
import { Input } from "@/components/input"
import { Skeleton } from "@/components/skeleton"
import { clx } from "@/utils/clx"
import * as React from "react"
import { useDataTableContext } from "@/blocks/data-table/context/use-data-table-context"
interface DataTableSearchProps {
autoFocus?: boolean
className?: string
placeholder?: string
}
const DataTableSearch = (props: DataTableSearchProps) => {
const { className, ...rest } = props
const { instance } = useDataTableContext()
if (!instance.enableSearch) {
throw new Error(
"DataTable.Search was rendered but search is not enabled. Make sure to pass search to 'useDataTable'"
)
}
if (instance.showSkeleton) {
return <DataTableSearchSkeleton />
}
return (
<Input
size="small"
type="search"
value={instance.getSearch()}
onChange={(e) => instance.onSearchChange(e.target.value)}
className={clx(
{
"pr-[calc(15px+2px+8px)]": instance.isLoading,
},
className
)}
{...rest}
/>
)
}
const DataTableSearchSkeleton = () => {
return <Skeleton className="h-7 w-[128px]" />
}
export { DataTableSearch }
export type { DataTableSearchProps }

View File

@@ -0,0 +1,49 @@
"use client"
import type { DataTableCellContext, DataTableHeaderContext } from "@/blocks/data-table/types"
import { Checkbox } from "@/components/checkbox"
import { CheckedState } from "@radix-ui/react-checkbox"
import * as React from "react"
interface DataTableSelectCellProps<TData> {
ctx: DataTableCellContext<TData, unknown>
}
const DataTableSelectCell = <TData,>(props: DataTableSelectCellProps<TData>) => {
const checked = props.ctx.row.getIsSelected()
const onChange = props.ctx.row.getToggleSelectedHandler()
return (
<Checkbox
onClick={(e) => e.stopPropagation()}
checked={checked}
onCheckedChange={onChange}
/>
)
}
interface DataTableSelectHeaderProps<TData> {
ctx: DataTableHeaderContext<TData, unknown>
}
const DataTableSelectHeader = <TData,>(props: DataTableSelectHeaderProps<TData>) => {
const checked = props.ctx.table.getIsSomePageRowsSelected()
? "indeterminate"
: props.ctx.table.getIsAllPageRowsSelected()
const onChange = (checked: CheckedState) => {
props.ctx.table.toggleAllPageRowsSelected(!!checked)
}
return (
<Checkbox
onClick={(e) => e.stopPropagation()}
checked={checked}
onCheckedChange={onChange}
/>
)
}
export { DataTableSelectCell, DataTableSelectHeader }
export type { DataTableSelectCellProps, DataTableSelectHeaderProps }

View File

@@ -0,0 +1,46 @@
"use client"
import { DataTableSortDirection } from "@/blocks/data-table/types"
import { clx } from "@/utils/clx"
import * as React from "react"
interface SortingIconProps {
direction: DataTableSortDirection | false
}
const DataTableSortingIcon = (props: SortingIconProps) => {
const isAscending = props.direction === "asc"
const isDescending = props.direction === "desc"
const isSorted = isAscending || isDescending
return (
<svg
width="16"
height="15"
viewBox="0 0 16 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={clx("opacity-0 transition-opacity group-hover:opacity-100", {
"opacity-100": isSorted,
})}
>
<path
d="M5.82651 5.75C5.66344 5.74994 5.50339 5.71269 5.36308 5.64216C5.22277 5.57162 5.10736 5.47039 5.02891 5.34904C4.95045 5.22769 4.91184 5.09067 4.9171 4.95232C4.92236 4.81397 4.97131 4.67936 5.05882 4.56255L7.64833 1.10788C7.73055 0.998207 7.84403 0.907911 7.97827 0.845354C8.11252 0.782797 8.26318 0.75 8.41632 0.75C8.56946 0.75 8.72013 0.782797 8.85437 0.845354C8.98862 0.907911 9.1021 0.998207 9.18432 1.10788L11.7744 4.56255C11.862 4.67939 11.9109 4.81405 11.9162 4.95245C11.9214 5.09085 11.8827 5.2279 11.8042 5.34926C11.7257 5.47063 11.6102 5.57185 11.4698 5.64235C11.3294 5.71285 11.1693 5.75003 11.0061 5.75H5.82651Z"
className={clx("fill-ui-fg-muted", {
"fill-ui-fg-subtle": isAscending,
})}
/>
<path
d="M11.0067 9.25C11.1698 9.25006 11.3299 9.28731 11.4702 9.35784C11.6105 9.42838 11.7259 9.52961 11.8043 9.65096C11.8828 9.77231 11.9214 9.90933 11.9162 10.0477C11.9109 10.186 11.8619 10.3206 11.7744 10.4374L9.18492 13.8921C9.10271 14.0018 8.98922 14.0921 8.85498 14.1546C8.72074 14.2172 8.57007 14.25 8.41693 14.25C8.26379 14.25 8.11312 14.2172 7.97888 14.1546C7.84464 14.0921 7.73115 14.0018 7.64894 13.8921L5.05882 10.4374C4.97128 10.3206 4.92233 10.1859 4.9171 10.0476C4.91186 9.90915 4.95053 9.7721 5.02905 9.65074C5.10758 9.52937 5.22308 9.42815 5.36347 9.35765C5.50387 9.28715 5.664 9.24997 5.82712 9.25H11.0067Z"
className={clx("fill-ui-fg-muted", {
"fill-ui-fg-subtle": isDescending,
})}
/>
</svg>
)
}
export { DataTableSortingIcon }
export type { SortingIconProps }

Some files were not shown because too many files have changed in this diff Show More