feat(dashboard,medusa,ui): Manual gift cards + cleanup (#6380)

**Dashboard**
- Adds different views for managing manual/custom gift cards (not associated with a product gift card).
- Cleans up several table implementations to use new DataTable component.
- Minor cleanup of translation file.

**Medusa**
- Adds missing query params for list endpoints in the following admin domains: /customers, /customer-groups, /collections, and /gift-cards.

**UI**
- Adds new sizes for Badge component.

**Note for review**
Since this PR contains updates to the translation keys, it touches a lot of files. For the review the parts that are relevant are: the /gift-cards domain of admin, the table overview of collections, customers, and customer groups. And the changes to the list endpoints in the core.
This commit is contained in:
Kasper Fabricius Kristensen
2024-02-12 14:47:37 +01:00
committed by GitHub
parent bc2a63782b
commit d37ff8024d
138 changed files with 3230 additions and 1399 deletions

View File

@@ -0,0 +1,51 @@
import { Customer } from "@medusajs/medusa"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import {
EmailCell,
EmailHeader,
} from "../../../components/table/table-cells/common/email-cell"
import {
NameCell,
NameHeader,
} from "../../../components/table/table-cells/common/name-cell"
import {
AccountCell,
AccountHeader,
} from "../../../components/table/table-cells/customer/account-cell/account-cell"
import {
FirstSeenCell,
FirstSeenHeader,
} from "../../../components/table/table-cells/customer/first-seen-cell"
const columnHelper = createColumnHelper<Customer>()
export const useCustomerTableColumns = () => {
return useMemo(
() => [
columnHelper.accessor("email", {
header: () => <EmailHeader />,
cell: ({ getValue }) => <EmailCell email={getValue()} />,
}),
columnHelper.display({
id: "name",
header: () => <NameHeader />,
cell: ({
row: {
original: { first_name, last_name },
},
}) => <NameCell firstName={first_name} lastName={last_name} />,
}),
columnHelper.accessor("has_account", {
header: () => <AccountHeader />,
cell: ({ getValue }) => <AccountCell hasAccount={getValue()} />,
}),
columnHelper.accessor("created_at", {
header: () => <FirstSeenHeader />,
cell: ({ getValue }) => <FirstSeenCell createdAt={getValue()} />,
}),
],
[]
)
}

View File

@@ -0,0 +1,59 @@
import { Product } from "@medusajs/medusa"
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import {
CollectionCell,
CollectionHeader,
} from "../../../components/table/table-cells/product/collection-cell/collection-cell"
import {
ProductCell,
ProductHeader,
} from "../../../components/table/table-cells/product/product-cell"
import {
ProductStatusCell,
ProductStatusHeader,
} from "../../../components/table/table-cells/product/product-status-cell"
import {
SalesChannelHeader,
SalesChannelsCell,
} from "../../../components/table/table-cells/product/sales-channels-cell"
import {
VariantCell,
VariantHeader,
} from "../../../components/table/table-cells/product/variant-cell"
const columnHelper = createColumnHelper<Product>()
export const useProductTableColumns = () => {
return useMemo(
() => [
columnHelper.display({
id: "product",
header: () => <ProductHeader />,
cell: ({ row }) => <ProductCell product={row.original} />,
}),
columnHelper.accessor("collection", {
header: () => <CollectionHeader />,
cell: ({ row }) => (
<CollectionCell collection={row.original.collection} />
),
}),
columnHelper.accessor("sales_channels", {
header: () => <SalesChannelHeader />,
cell: ({ row }) => (
<SalesChannelsCell salesChannels={row.original.sales_channels} />
),
}),
columnHelper.accessor("variants", {
header: () => <VariantHeader />,
cell: ({ row }) => <VariantCell variants={row.original.variants} />,
}),
columnHelper.accessor("status", {
header: () => <ProductStatusHeader />,
cell: ({ row }) => <ProductStatusCell status={row.original.status} />,
}),
],
[]
) as ColumnDef<Product>[]
}

View File

@@ -0,0 +1,69 @@
import { useAdminCustomerGroups } from "medusa-react"
import { useTranslation } from "react-i18next"
import { Filter } from "../../../components/table/data-table"
const excludeableFields = ["groups"] as const
export const useCustomerTableFilters = (
exclude?: (typeof excludeableFields)[number][]
) => {
const { t } = useTranslation()
const isGroupsExcluded = exclude?.includes("groups")
const { customer_groups } = useAdminCustomerGroups(
{
limit: 1000,
expand: "",
},
{
enabled: !isGroupsExcluded,
}
)
let filters: Filter[] = []
if (customer_groups && !isGroupsExcluded) {
const customerGroupFilter: Filter = {
key: "groups",
label: t("customers.groups"),
type: "select",
multiple: true,
options: customer_groups.map((s) => ({
label: s.name,
value: s.id,
})),
}
filters = [...filters, customerGroupFilter]
}
const hasAccountFilter: Filter = {
key: "has_account",
label: t("fields.account"),
type: "select",
options: [
{
label: t("customers.registered"),
value: "true",
},
{
label: t("customers.guest"),
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, hasAccountFilter, ...dateFilters]
return filters
}

View File

@@ -0,0 +1,190 @@
import {
useAdminCollections,
useAdminProductCategories,
useAdminProductTags,
useAdminProductTypes,
useAdminSalesChannels,
} from "medusa-react"
import { useTranslation } from "react-i18next"
import { Filter } from "../../../components/table/data-table"
const excludeableFields = ["sales_channel_id", "collections"] as const
export const useProductTableFilters = (
exclude?: (typeof excludeableFields)[number][]
) => {
const { t } = useTranslation()
const { product_types } = useAdminProductTypes({
limit: 1000,
offset: 0,
})
const { product_tags } = useAdminProductTags({
limit: 1000,
offset: 0,
})
const isSalesChannelExcluded = exclude?.includes("sales_channel_id")
const { sales_channels } = useAdminSalesChannels(
{
limit: 1000,
fields: "id,name",
expand: "",
},
{
enabled: !isSalesChannelExcluded,
}
)
const { product_categories } = useAdminProductCategories({
limit: 1000,
offset: 0,
fields: "id,name",
expand: "",
})
const isCollectionExcluded = exclude?.includes("collections")
const { collections } = useAdminCollections(
{
limit: 1000,
offset: 0,
},
{
enabled: !isCollectionExcluded,
}
)
let filters: Filter[] = []
if (product_types) {
const typeFilter: Filter = {
key: "type_id",
label: t("fields.type"),
type: "select",
multiple: true,
options: product_types.map((t) => ({
label: t.value,
value: t.id,
})),
}
filters = [...filters, typeFilter]
}
if (product_tags) {
const tagFilter: Filter = {
key: "tags",
label: t("fields.tag"),
type: "select",
multiple: true,
options: product_tags.map((t) => ({
label: t.value,
value: t.id,
})),
}
filters = [...filters, tagFilter]
}
if (sales_channels) {
const salesChannelFilter: Filter = {
key: "sales_channel_id",
label: t("fields.salesChannel"),
type: "select",
multiple: true,
options: sales_channels.map((s) => ({
label: s.name,
value: s.id,
})),
}
filters = [...filters, salesChannelFilter]
}
if (product_categories) {
const categoryFilter: Filter = {
key: "category_id",
label: t("fields.category"),
type: "select",
multiple: true,
options: product_categories.map((c) => ({
label: c.name,
value: c.id,
})),
}
filters = [...filters, categoryFilter]
}
if (collections) {
const collectionFilter: Filter = {
key: "collection_id",
label: t("fields.collection"),
type: "select",
multiple: true,
options: collections.map((c) => ({
label: c.title,
value: c.id,
})),
}
filters = [...filters, collectionFilter]
}
const giftCardFilter: Filter = {
key: "is_giftcard",
label: t("fields.giftCard"),
type: "select",
options: [
{
label: t("fields.true"),
value: "true",
},
{
label: t("fields.false"),
value: "false",
},
],
}
const statusFilter: Filter = {
key: "status",
label: t("fields.status"),
type: "select",
multiple: true,
options: [
{
label: t("products.productStatus.draft"),
value: "draft",
},
{
label: t("products.productStatus.proposed"),
value: "proposed",
},
{
label: t("products.productStatus.published"),
value: "published",
},
{
label: t("products.productStatus.rejected"),
value: "rejected",
},
],
}
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, statusFilter, giftCardFilter, ...dateFilters]
return filters
}

View File

@@ -0,0 +1,47 @@
import { AdminGetCustomersParams } from "@medusajs/medusa"
import { useQueryParams } from "../../use-query-params"
type UseCustomerTableQueryProps = {
prefix?: string
pageSize?: number
}
export const useCustomerTableQuery = ({
prefix,
pageSize = 20,
}: UseCustomerTableQueryProps) => {
const queryObject = useQueryParams(
[
"offset",
"q",
"has_account",
"groups",
"order",
"created_at",
"updated_at",
],
prefix
)
const { offset, groups, has_account, q, order } = queryObject
const searchParams: AdminGetCustomersParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
groups: groups?.split(","),
has_account: has_account ? has_account === "true" : undefined,
order,
created_at: queryObject.created_at
? JSON.parse(queryObject.created_at)
: undefined,
updated_at: queryObject.updated_at
? JSON.parse(queryObject.updated_at)
: undefined,
q,
}
return {
searchParams,
raw: queryObject,
}
}

View File

@@ -6,13 +6,9 @@ type UseOrderTableQueryProps = {
pageSize?: number
}
/**
* TODO: Enable `order` query param when staging is updated
*/
export const useOrderTableQuery = ({
prefix,
pageSize = 50,
pageSize = 20,
}: UseOrderTableQueryProps) => {
const queryObject = useQueryParams(
[
@@ -24,6 +20,7 @@ export const useOrderTableQuery = ({
"sales_channel_id",
"payment_status",
"fulfillment_status",
"order",
],
prefix
)

View File

@@ -0,0 +1,68 @@
import { AdminGetProductsParams } from "@medusajs/medusa"
import { ProductStatus } from "@medusajs/types"
import { useQueryParams } from "../../use-query-params"
type UseProductTableQueryProps = {
prefix?: string
pageSize?: number
}
export const useProductTableQuery = ({
prefix,
pageSize = 20,
}: UseProductTableQueryProps) => {
const queryObject = useQueryParams(
[
"offset",
"order",
"q",
"created_at",
"updated_at",
"sales_channel_id",
"category_id",
"collection_id",
"is_giftcard",
"tags",
"type_id",
"status",
],
prefix
)
const {
offset,
sales_channel_id,
created_at,
updated_at,
category_id,
collection_id,
tags,
type_id,
is_giftcard,
status,
order,
q,
} = queryObject
const searchParams: AdminGetProductsParams = {
limit: pageSize,
offset: offset ? Number(offset) : 0,
sales_channel_id: sales_channel_id?.split(","),
created_at: created_at ? JSON.parse(created_at) : undefined,
updated_at: updated_at ? JSON.parse(updated_at) : undefined,
category_id: category_id?.split(","),
collection_id: collection_id?.split(","),
is_giftcard: is_giftcard ? is_giftcard === "true" : undefined,
order: order,
tags: tags?.split(","),
type_id: type_id?.split(","),
status: status?.split(",") as ProductStatus[],
q,
}
return {
searchParams,
raw: queryObject,
}
}