From d37ff8024d8affbe84db3c0b6d79cd41016bfac4 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Mon, 12 Feb 2024 14:47:37 +0100 Subject: [PATCH] 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. --- .changeset/quiet-cougars-pretend.md | 7 + .../locales/{en => en-US}/translation.json | 62 +++- .../json-view-section/json-view-section.tsx | 4 +- .../localized-date-picker.tsx | 2 +- .../data-table-root/data-table-root.tsx | 21 +- .../table/data-table/data-table.tsx | 8 +- .../common/email-cell/email-cell.tsx | 28 ++ .../table-cells/common/email-cell/index.ts | 1 + .../common/money-amount-cell/index.ts | 1 + .../money-amount-cell/money-amount-cell.tsx | 45 +++ .../table-cells/common/name-cell/index.ts | 1 + .../common/name-cell/name-cell.tsx | 31 ++ .../common/placeholder-cell/index.ts | 1 + .../placeholder-cell/placeholder-cell.tsx | 7 + .../customer/account-cell/account-cell.tsx | 25 ++ .../first-seen-cell/first-seen-cell.tsx | 20 ++ .../customer/first-seen-cell/index.ts | 1 + .../order/display-id-cell/display-id-cell.tsx | 7 +- .../order/total-cell/total-cell.tsx | 24 +- .../collection-cell/collection-cell.tsx | 30 ++ .../product/collection-cell/index.ts | 1 + .../table-cells/product/product-cell/index.ts | 1 + .../product/product-cell/product-cell.tsx | 30 ++ .../product/product-status-cell/index.ts | 1 + .../product-status-cell.tsx | 31 ++ .../product/sales-channels-cell/index.ts | 1 + .../sales-channels-cell.tsx | 65 ++++ .../table-cells/product/variant-cell/index.ts | 1 + .../product/variant-cell/variant-cell.tsx | 34 ++ .../columns/use-customer-table-columns.tsx | 51 +++ .../columns/use-product-table-columns.tsx | 59 ++++ .../filters/use-customer-table-filters.tsx | 69 ++++ .../filters/use-product-table-filters.tsx | 190 +++++++++++ .../table/query/use-customer-table-query.tsx | 47 +++ .../table/query/use-order-table-query.tsx | 7 +- .../table/query/use-product-table-query.tsx | 68 ++++ .../dashboard/src/hooks/use-data-table.tsx | 13 +- .../dashboard/src/hooks/use-form-prompt.tsx | 4 +- .../src/hooks/use-route-modal-state.tsx | 14 +- .../admin-next/dashboard/src/i18n/config.ts | 13 +- .../admin-next/dashboard/src/i18n/types.ts | 2 +- .../admin-next/dashboard/src/i18next.d.ts | 4 +- .../router-provider/router-provider.tsx | 24 +- .../add-sales-channels-to-api-key-form.tsx | 8 +- .../create-publishable-api-key-form.tsx | 6 +- .../api-key-general-section.tsx | 26 +- .../api-key-sales-channel-section.tsx | 22 +- .../edit-api-key-form/edit-api-key-form.tsx | 4 +- .../api-key-management-list.tsx | 2 +- .../api-key-management-list-table.tsx | 18 +- .../add-products-to-collection-form.tsx | 12 +- .../create-collection-form.tsx | 8 +- .../collection-general-section.tsx | 14 +- .../collection-product-section.tsx | 261 +++------------ .../edit-collection-form.tsx | 6 +- .../collection-list-table.tsx | 237 ++----------- .../use-collection-table-columns.tsx | 94 ++++++ .../use-collection-table-filters.tsx | 21 ++ .../use-collection-table-query.tsx | 33 ++ .../add-customers-form/add-customers-form.tsx | 12 +- .../create-customer-group-form.tsx | 8 +- .../customer-group-customer-section.tsx | 268 ++++----------- .../customer-group-general-section.tsx | 6 +- .../edit-customer-group-form.tsx | 8 +- .../customer-group-list-table.tsx | 263 +++------------ .../use-customer-group-table-columns.tsx | 85 +++++ .../use-customer-group-table-filters.tsx | 21 ++ .../use-customer-group-table-query.tsx | 33 ++ .../create-customer-form.tsx | 6 +- .../customer-general-section.tsx | 99 +++--- .../customer-order-section.tsx | 2 +- .../edit-customer-form/edit-customer-form.tsx | 4 +- .../customer-list-table.tsx | 211 +++--------- .../src/routes/gift-cards/details/details.tsx | 3 - .../src/routes/gift-cards/details/index.ts | 1 - .../create-gift-card-form.tsx | 311 ++++++++++++++++++ .../components/create-gift-card-form/index.ts | 1 + .../gift-card-create/gift-card-create.tsx | 15 + .../gift-cards/gift-card-create/index.ts | 1 + .../gift-card-general-section.tsx | 194 +++++++++++ .../gift-card-general-section/index.ts | 1 + .../gift-card-detail/gift-card-detail.tsx | 33 ++ .../gift-cards/gift-card-detail/index.ts | 2 + .../gift-cards/gift-card-detail/loader.ts | 21 ++ .../edit-gift-card-form.tsx | 277 ++++++++++++++++ .../components/edit-gift-card-form/index.ts | 1 + .../gift-card-edit/gift-card-edit.tsx | 39 +++ .../routes/gift-cards/gift-card-edit/index.ts | 1 + .../gift-card-list-table.tsx | 68 ++++ .../components/gift-card-list-table/index.ts | 1 + .../use-gift-card-table-columns.tsx | 136 ++++++++ .../use-gift-card-table-filters.tsx | 21 ++ .../use-gift-card-table-query.tsx | 19 ++ .../gift-card-list/gift-card-list.tsx | 11 + .../routes/gift-cards/gift-card-list/index.ts | 1 + .../src/routes/gift-cards/list/index.ts | 1 - .../src/routes/gift-cards/list/list.tsx | 9 - .../create-location-form.tsx | 4 +- .../location-sales-channel-section.tsx | 8 +- .../edit-location-form/edit-location-form.tsx | 4 +- .../locations-list-table.tsx | 10 +- .../dashboard/src/routes/login/login.tsx | 4 +- .../order-list-table/order-list-table.tsx | 2 +- .../product-list-table/product-list-table.tsx | 8 +- .../profile-general-section.tsx | 12 +- .../edit-profile-form/edit-profile-form.tsx | 4 +- .../create-region-form/create-region-form.tsx | 8 +- .../region-general-section.tsx | 31 +- .../edit-region-form/edit-region-form.tsx | 4 +- .../region-list-table/region-list-table.tsx | 10 +- .../add-products-to-sales-channel-form.tsx | 8 +- .../create-sales-channel-form.tsx | 4 +- .../sales-channel-general-section.tsx | 8 +- .../sales-channel-product-section.tsx | 12 +- .../edit-sales-channel-form.tsx | 8 +- .../sales-channel-list-table.tsx | 14 +- .../add-currencies-form.tsx | 6 +- .../store-currency-section.tsx | 12 +- .../store-general-section.tsx | 10 +- .../store/store-detail/store-detail.tsx | 3 - .../edit-store-form/edit-store-form.tsx | 4 +- .../user-general-section.tsx | 71 +++- .../edit-user-form/edit-user-form.tsx | 8 +- .../invite-user-form/invite-user-form.tsx | 26 +- .../user-list-table/user-list-table.tsx | 10 +- .../ui/src/components/badge/badge.stories.tsx | 12 + .../ui/src/components/badge/badge.tsx | 62 +++- .../currency-input/currency-input.tsx | 4 + .../lib/models/AdminGetCollectionsParams.ts | 4 + .../models/AdminGetCustomerGroupsParams.ts | 4 + .../src/lib/models/AdminGetCustomersParams.ts | 56 +++- .../src/lib/models/AdminGetGiftCardsParams.ts | 4 + .../admin/collections/list-collections.ts | 12 +- .../customer-groups/list-customer-groups.ts | 10 +- .../routes/admin/customers/list-customers.ts | 61 +++- .../admin/gift-cards/list-gift-cards.ts | 12 +- .../controllers/customers/list-customers.ts | 19 +- packages/medusa/src/types/customers.ts | 34 +- 138 files changed, 3230 insertions(+), 1399 deletions(-) create mode 100644 .changeset/quiet-cougars-pretend.md rename packages/admin-next/dashboard/public/locales/{en => en-US}/translation.json (88%) create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/email-cell/email-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/email-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/name-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/name-cell/name-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/placeholder-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/common/placeholder-cell/placeholder-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/customer/account-cell/account-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/customer/first-seen-cell/first-seen-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/customer/first-seen-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/product/collection-cell/collection-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/product/collection-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/product/product-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/product/product-cell/product-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/product/product-status-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/product/product-status-cell/product-status-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/product/sales-channels-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/product/sales-channels-cell/sales-channels-cell.tsx create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/product/variant-cell/index.ts create mode 100644 packages/admin-next/dashboard/src/components/table/table-cells/product/variant-cell/variant-cell.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/table/columns/use-customer-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/table/columns/use-product-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/table/filters/use-customer-table-filters.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/table/query/use-customer-table-query.tsx create mode 100644 packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx create mode 100644 packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-filters.tsx create mode 100644 packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-query.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-filters.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-query.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/details/details.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/details/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-create/components/create-gift-card-form/create-gift-card-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-create/components/create-gift-card-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-create/gift-card-create.tsx create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-create/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-detail/components/gift-card-general-section/gift-card-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-detail/components/gift-card-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-detail/gift-card-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-detail/loader.ts create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-edit/components/edit-gift-card-form/edit-gift-card-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-edit/components/edit-gift-card-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-edit/gift-card-edit.tsx create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-list/components/gift-card-list-table/gift-card-list-table.tsx create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-list/components/gift-card-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-list/components/gift-card-list-table/use-gift-card-table-columns.tsx create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-list/components/gift-card-list-table/use-gift-card-table-filters.tsx create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-list/components/gift-card-list-table/use-gift-card-table-query.tsx create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-list/gift-card-list.tsx create mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/gift-card-list/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/list/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/gift-cards/list/list.tsx diff --git a/.changeset/quiet-cougars-pretend.md b/.changeset/quiet-cougars-pretend.md new file mode 100644 index 0000000000..40e958bbfa --- /dev/null +++ b/.changeset/quiet-cougars-pretend.md @@ -0,0 +1,7 @@ +--- +"@medusajs/client-types": patch +"@medusajs/ui": patch +"@medusajs/medusa": patch +--- + +fix(medusa,ui): Fixes list query params for the following endpoints: "/admin/customers", "/admin/customer-groups", "/admin/gift-cards", and "/admin/collections". diff --git a/packages/admin-next/dashboard/public/locales/en/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json similarity index 88% rename from packages/admin-next/dashboard/public/locales/en/translation.json rename to packages/admin-next/dashboard/public/locales/en-US/translation.json index b84fd893ee..57922e0724 100644 --- a/packages/admin-next/dashboard/public/locales/en/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -3,16 +3,7 @@ "general": { "ascending": "Ascending", "descending": "Descending", - "cancel": "Cancel", - "close": "Close", - "save": "Save", - "create": "Create", - "delete": "Delete", - "invite": "Invite", - "edit": "Edit", - "confirm": "Confirm", "add": "Add", - "continue": "Continue", "start": "Start", "end": "End", "apply": "Apply", @@ -30,10 +21,9 @@ "details": "Details", "enabled": "Enabled", "disabled": "Disabled", + "expired": "Expired", "active": "Active", - "revoke": "Revoke", "revoked": "Revoked", - "remove": "Remove", "admin": "Admin", "store": "Store", "items_one": "{{count}} item", @@ -50,6 +40,16 @@ "unsavedChangesTitle": "Are you sure you want to leave this page?", "unsavedChangesDescription": "You have unsaved changes that will be lost if you leave this page." }, + "actions": { + "create": "Create", + "delete": "Delete", + "remove": "Remove", + "revoke": "Revoke", + "cancel": "Cancel", + "save": "Save", + "continue": "Continue", + "edit": "Edit" + }, "products": { "domain": "Products", "variants": "Variants", @@ -69,8 +69,7 @@ "createCollectionHint": "Create a new collection to organize your products.", "editCollection": "Edit Collection", "handleTooltip": "The handle is used to reference the collection in your storefront. If not specified, the handle will be generated from the collection title.", - "deleteWarning_one": "You are about to delete {{count}} collection. This action cannot be undone.", - "deleteWarning_other": "You are about to delete {{count}} collections. This action cannot be undone.", + "deleteWarning": "You are about to delete the collection {{title}}. This action cannot be undone.", "removeSingleProductWarning": "You are about to remove the product {{title}} from the collection. This action cannot be undone.", "removeProductsWarning_one": "You are about to remove {{count}} product from the collection. This action cannot be undone.", "removeProductsWarning_other": "You are about to remove {{count}} products from the collection. This action cannot be undone." @@ -82,7 +81,22 @@ "domain": "Inventory" }, "giftCards": { - "domain": "Gift Cards" + "domain": "Gift Cards", + "editGiftCard": "Edit Gift Card", + "createGiftCard": "Create Gift Card", + "createGiftCardHint": "Manually create a gift card that can be used as a payment method in your store.", + "selectRegionFirst": "Select a region first", + "deleteGiftCardWarning": "You are about to delete the gift card {{code}}. This action cannot be undone.", + "balanceHigherThanValue": "The balance cannot be higher than the original amount.", + "balanceLowerThanZero": "The balance cannot be negative.", + "expiryDateHint": "Countries have different laws regarding gift card expiry dates. Make sure to check local regulations before setting an expiry date.", + "regionHint": "Changing the region of the gift card will also change its currency, potentially affecting its monetary value.", + "enabledHint": "Specify if the gift card is enabled or disabled.", + "balance": "Balance", + "currentBalance": "Current balance", + "initialBalance": "Initial balance", + "personalMessage": "Personal message", + "recipient": "Recipient" }, "customers": { "domain": "Customers", @@ -96,7 +110,8 @@ "guest": "Guest", "registered": "Registered", "firstSeen": "First seen", - "viewOrder": "View order" + "viewOrder": "View order", + "groups": "Groups" }, "customerGroups": { "domain": "Customer Groups", @@ -174,7 +189,9 @@ "admin": "Admin", "developer": "Developer", "member": "Member" - } + }, + "deleteUserWarning": "You are about to delete the user {{name}}. This action cannot be undone.", + "invite": "Invite" }, "store": { "domain": "Store", @@ -337,6 +354,17 @@ "salesChannel": "Sales Channel", "region": "Region", "role": "Role", - "sent": "Sent" + "sent": "Sent", + "salesChannels": "Sales Channels", + "product": "Product", + "createdAt": "Created at", + "updatedAt": "Updated at", + "true": "True", + "false": "False", + "giftCard": "Gift Card", + "tag": "Tag", + "dateIssued": "Date issued", + "issuedDate": "Issued date", + "expiryDate": "Expiry date" } } diff --git a/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx b/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx index 7727b9f663..18b5ffaa1c 100644 --- a/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx +++ b/packages/admin-next/dashboard/src/components/common/json-view-section/json-view-section.tsx @@ -28,7 +28,7 @@ export const JsonViewSection = ({ data, root }: JsonViewProps) => {
JSON - {numberOfKeys} keys + {numberOfKeys} keys
@@ -44,7 +44,7 @@ export const JsonViewSection = ({ data, root }: JsonViewProps) => {
JSON - {numberOfKeys} keys + {numberOfKeys} keys
esc diff --git a/packages/admin-next/dashboard/src/components/localization/localized-date-picker/localized-date-picker.tsx b/packages/admin-next/dashboard/src/components/localization/localized-date-picker/localized-date-picker.tsx index 8517f15817..892c726ea9 100644 --- a/packages/admin-next/dashboard/src/components/localization/localized-date-picker/localized-date-picker.tsx +++ b/packages/admin-next/dashboard/src/components/localization/localized-date-picker/localized-date-picker.tsx @@ -18,7 +18,7 @@ export const LocalizedDatePicker = ({ ?.date_locale const translations = { - cancel: t("general.cancel"), + cancel: t("actions.cancel"), apply: t("general.apply"), end: t("general.end"), start: t("general.start"), diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx index 374cc0b2be..c0786e85bf 100644 --- a/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx @@ -13,10 +13,10 @@ import { NoResults } from "../../../common/empty-table-content" type BulkCommand = { label: string shortcut: string - action: (selection: Record) => void + action: (selection: Record) => Promise } -export interface DataTableRootProps { +export interface DataTableRootProps { /** * The table instance to render */ @@ -24,7 +24,7 @@ export interface DataTableRootProps { /** * The columns to render */ - columns: ColumnDef[] + columns: ColumnDef[] /** * Function to generate a link to navigate to when clicking on a row */ @@ -61,7 +61,7 @@ export interface DataTableRootProps { /** * Table component for rendering a table with pagination, filtering and ordering. */ -export const DataTableRoot = ({ +export const DataTableRoot = ({ table, columns, pagination, @@ -69,7 +69,7 @@ export const DataTableRoot = ({ commands, count = 0, noResults = false, -}: DataTableRootProps) => { +}: DataTableRootProps) => { const { t } = useTranslation() const navigate = useNavigate() const [showStickyBorder, setShowStickyBorder] = useState(false) @@ -94,6 +94,12 @@ export const DataTableRoot = ({ } } + const handleAction = async (action: BulkCommand["action"]) => { + await action(rowSelection).then(() => { + table.resetRowSelection() + }) + } + return (
@@ -159,6 +165,7 @@ export const DataTableRoot = ({ return ( ({ ({ command.action(rowSelection)} + action={() => handleAction(command.action)} /> {index < commands.length - 1 && } diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx index 09446bb367..e603357906 100644 --- a/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx @@ -4,8 +4,8 @@ import { DataTableQuery, DataTableQueryProps } from "./data-table-query" import { DataTableRoot, DataTableRootProps } from "./data-table-root" import { DataTableSkeleton } from "./data-table-skeleton" -interface DataTableProps - extends DataTableRootProps, +interface DataTableProps + extends DataTableRootProps, DataTableQueryProps { isLoading?: boolean rowCount: number @@ -15,7 +15,7 @@ interface DataTableProps const MemoizedDataTableRoot = memo(DataTableRoot) as typeof DataTableRoot const MemoizedDataTableQuery = memo(DataTableQuery) -export const DataTable = ({ +export const DataTable = ({ table, columns, pagination, @@ -29,7 +29,7 @@ export const DataTable = ({ queryObject = {}, rowCount, isLoading = false, -}: DataTableProps) => { +}: DataTableProps) => { if (isLoading) { return ( { + if (!email) { + return + } + + return ( +
+ {email} +
+ ) +} + +export const EmailHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.email")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/email-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/common/email-cell/index.ts new file mode 100644 index 0000000000..a65676ca82 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/email-cell/index.ts @@ -0,0 +1 @@ +export * from "./email-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/index.ts new file mode 100644 index 0000000000..9c55954207 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/index.ts @@ -0,0 +1 @@ +export * from "./money-amount-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx new file mode 100644 index 0000000000..57a9cf0e66 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/money-amount-cell/money-amount-cell.tsx @@ -0,0 +1,45 @@ +import { clx } from "@medusajs/ui" +import { getPresentationalAmount } from "../../../../../lib/money-amount-helpers" +import { PlaceholderCell } from "../placeholder-cell" + +type MoneyAmountCellProps = { + currencyCode: string + amount?: number | null + align?: "left" | "right" +} + +export const MoneyAmountCell = ({ + currencyCode, + amount, + align = "left", +}: MoneyAmountCellProps) => { + if (!amount) { + return + } + + const formatted = new Intl.NumberFormat(undefined, { + style: "currency", + currency: currencyCode, + currencyDisplay: "narrowSymbol", + }).format(0) + + const symbol = formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim() + + const presentationAmount = getPresentationalAmount(amount, currencyCode) + const formattedTotal = new Intl.NumberFormat(undefined, { + style: "decimal", + }).format(presentationAmount) + + return ( +
+ + {symbol} {formattedTotal} {currencyCode.toUpperCase()} + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/name-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/common/name-cell/index.ts new file mode 100644 index 0000000000..9002871f49 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/name-cell/index.ts @@ -0,0 +1 @@ +export * from "./name-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/name-cell/name-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/name-cell/name-cell.tsx new file mode 100644 index 0000000000..d9fce020e7 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/name-cell/name-cell.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from "react-i18next" +import { PlaceholderCell } from "../placeholder-cell" + +type NameCellProps = { + firstName?: string | null + lastName?: string | null +} + +export const NameCell = ({ firstName, lastName }: NameCellProps) => { + if (!firstName && !lastName) { + return + } + + const name = [firstName, lastName].filter(Boolean).join(" ") + + return ( +
+ {name} +
+ ) +} + +export const NameHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.name")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/placeholder-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/common/placeholder-cell/index.ts new file mode 100644 index 0000000000..2991d7f09a --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/placeholder-cell/index.ts @@ -0,0 +1 @@ +export * from "./placeholder-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/placeholder-cell/placeholder-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/placeholder-cell/placeholder-cell.tsx new file mode 100644 index 0000000000..1deeb9bbd8 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/placeholder-cell/placeholder-cell.tsx @@ -0,0 +1,7 @@ +export const PlaceholderCell = () => { + return ( +
+ - +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/customer/account-cell/account-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/customer/account-cell/account-cell.tsx new file mode 100644 index 0000000000..4dbc983865 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/customer/account-cell/account-cell.tsx @@ -0,0 +1,25 @@ +import { useTranslation } from "react-i18next" +import { StatusCell } from "../../common/status-cell" + +type AccountCellProps = { + hasAccount: boolean +} + +export const AccountCell = ({ hasAccount }: AccountCellProps) => { + const { t } = useTranslation() + + const color = hasAccount ? "green" : ("orange" as const) + const text = hasAccount ? t("customers.registered") : t("customers.guest") + + return {text} +} + +export const AccountHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.account")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/customer/first-seen-cell/first-seen-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/customer/first-seen-cell/first-seen-cell.tsx new file mode 100644 index 0000000000..6b508d5e7a --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/customer/first-seen-cell/first-seen-cell.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from "react-i18next" +import { DateCell } from "../../common/date-cell" + +type FirstSeenCellProps = { + createdAt: Date +} + +export const FirstSeenCell = ({ createdAt }: FirstSeenCellProps) => { + return +} + +export const FirstSeenHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("customers.firstSeen")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/customer/first-seen-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/customer/first-seen-cell/index.ts new file mode 100644 index 0000000000..7a01262aa2 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/customer/first-seen-cell/index.ts @@ -0,0 +1 @@ +export * from "./first-seen-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/display-id-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/display-id-cell.tsx index e4f2e48aeb..97d7106b4d 100644 --- a/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/display-id-cell.tsx +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/display-id-cell.tsx @@ -1,6 +1,11 @@ import { useTranslation } from "react-i18next" +import { PlaceholderCell } from "../../common/placeholder-cell" + +export const DisplayIdCell = ({ displayId }: { displayId?: number | null }) => { + if (!displayId) { + return + } -export const DisplayIdCell = ({ displayId }: { displayId: number }) => { return (
#{displayId} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/total-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/total-cell.tsx index 58646ee933..f9543ad99d 100644 --- a/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/total-cell.tsx +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/total-cell.tsx @@ -1,5 +1,6 @@ import { useTranslation } from "react-i18next" -import { getPresentationalAmount } from "../../../../../lib/money-amount-helpers" +import { MoneyAmountCell } from "../../common/money-amount-cell" +import { PlaceholderCell } from "../../common/placeholder-cell" type TotalCellProps = { currencyCode: string @@ -8,28 +9,11 @@ type TotalCellProps = { export const TotalCell = ({ currencyCode, total }: TotalCellProps) => { if (!total) { - return - + return } - const formatted = new Intl.NumberFormat(undefined, { - style: "currency", - currency: currencyCode, - currencyDisplay: "narrowSymbol", - }).format(0) - - const symbol = formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim() - - const presentationAmount = getPresentationalAmount(total, currencyCode) - const formattedTotal = new Intl.NumberFormat(undefined, { - style: "decimal", - }).format(presentationAmount) - return ( -
- - {symbol} {formattedTotal} {currencyCode.toUpperCase()} - -
+ ) } diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/product/collection-cell/collection-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/product/collection-cell/collection-cell.tsx new file mode 100644 index 0000000000..3d714b29b1 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/product/collection-cell/collection-cell.tsx @@ -0,0 +1,30 @@ +import type { ProductCollection } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" + +import { PlaceholderCell } from "../../common/placeholder-cell" + +type CollectionCellProps = { + collection?: ProductCollection | null +} + +export const CollectionCell = ({ collection }: CollectionCellProps) => { + if (!collection) { + return + } + + return ( +
+ {collection.title} +
+ ) +} + +export const CollectionHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.collection")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/product/collection-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/product/collection-cell/index.ts new file mode 100644 index 0000000000..c6184e9938 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/product/collection-cell/index.ts @@ -0,0 +1 @@ +export * from "./collection-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/product/product-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/product/product-cell/index.ts new file mode 100644 index 0000000000..d2d7b28355 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/product/product-cell/index.ts @@ -0,0 +1 @@ +export * from "./product-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/product/product-cell/product-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/product/product-cell/product-cell.tsx new file mode 100644 index 0000000000..d6e974993b --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/product/product-cell/product-cell.tsx @@ -0,0 +1,30 @@ +import type { Product } from "@medusajs/medusa" +import type { PricedProduct } from "@medusajs/medusa/dist/types/pricing" +import { useTranslation } from "react-i18next" + +import { Thumbnail } from "../../../../common/thumbnail" + +type ProductCellProps = { + product: Product | PricedProduct +} + +export const ProductCell = ({ product }: ProductCellProps) => { + return ( +
+
+ +
+ {product.title} +
+ ) +} + +export const ProductHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.product")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/product/product-status-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/product/product-status-cell/index.ts new file mode 100644 index 0000000000..c4fd9163f0 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/product/product-status-cell/index.ts @@ -0,0 +1 @@ +export * from "./product-status-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/product/product-status-cell/product-status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/product/product-status-cell/product-status-cell.tsx new file mode 100644 index 0000000000..770a1da932 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/product/product-status-cell/product-status-cell.tsx @@ -0,0 +1,31 @@ +import { ProductStatus } from "@medusajs/types" +import { useTranslation } from "react-i18next" + +import { StatusCell } from "../../common/status-cell" + +type ProductStatusCellProps = { + status: ProductStatus +} + +export const ProductStatusCell = ({ status }: ProductStatusCellProps) => { + const { t } = useTranslation() + + const [color, text] = { + draft: ["grey", t("products.productStatus.draft")], + proposed: ["orange", t("products.productStatus.proposed")], + published: ["green", t("products.productStatus.published")], + rejected: ["red", t("products.productStatus.rejected")], + }[status] as ["grey" | "orange" | "green" | "red", string] + + return {text} +} + +export const ProductStatusHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.status")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/product/sales-channels-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/product/sales-channels-cell/index.ts new file mode 100644 index 0000000000..9beda9262a --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/product/sales-channels-cell/index.ts @@ -0,0 +1 @@ +export * from "./sales-channels-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/product/sales-channels-cell/sales-channels-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/product/sales-channels-cell/sales-channels-cell.tsx new file mode 100644 index 0000000000..af1408c386 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/product/sales-channels-cell/sales-channels-cell.tsx @@ -0,0 +1,65 @@ +import type { SalesChannel } from "@medusajs/medusa" +import { Tooltip } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +import { PlaceholderCell } from "../../common/placeholder-cell" + +type SalesChannelsCellProps = { + salesChannels?: SalesChannel[] | null +} + +export const SalesChannelsCell = ({ + salesChannels, +}: SalesChannelsCellProps) => { + const { t } = useTranslation() + + if (!salesChannels || !salesChannels.length) { + return + } + + if (salesChannels.length > 2) { + return ( +
+ + {salesChannels + .slice(0, 2) + .map((sc) => sc.name) + .join(", ")} + + + {salesChannels.slice(2).map((sc) => ( +
  • {sc.name}
  • + ))} + + } + > + + {t("general.plusCountMore", { + count: salesChannels.length - 2, + })} + +
    +
    + ) + } + + return ( +
    + + {salesChannels.map((sc) => sc.name).join(", ")} + +
    + ) +} + +export const SalesChannelHeader = () => { + const { t } = useTranslation() + + return ( +
    + {t("fields.salesChannels")} +
    + ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/product/variant-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/product/variant-cell/index.ts new file mode 100644 index 0000000000..3a565aa391 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/product/variant-cell/index.ts @@ -0,0 +1 @@ +export * from "./variant-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/product/variant-cell/variant-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/product/variant-cell/variant-cell.tsx new file mode 100644 index 0000000000..dce5d0a80f --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/product/variant-cell/variant-cell.tsx @@ -0,0 +1,34 @@ +import { ProductVariant } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" + +import { PlaceholderCell } from "../../common/placeholder-cell" + +type VariantCellProps = { + variants?: ProductVariant[] | null +} + +export const VariantCell = ({ variants }: VariantCellProps) => { + const { t } = useTranslation() + + if (!variants || !variants.length) { + return + } + + return ( +
    + + {t("products.variantCount", { count: variants.length })} + +
    + ) +} + +export const VariantHeader = () => { + const { t } = useTranslation() + + return ( +
    + {t("fields.variants")} +
    + ) +} diff --git a/packages/admin-next/dashboard/src/hooks/table/columns/use-customer-table-columns.tsx b/packages/admin-next/dashboard/src/hooks/table/columns/use-customer-table-columns.tsx new file mode 100644 index 0000000000..d8bdb47826 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/columns/use-customer-table-columns.tsx @@ -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() + +export const useCustomerTableColumns = () => { + return useMemo( + () => [ + columnHelper.accessor("email", { + header: () => , + cell: ({ getValue }) => , + }), + columnHelper.display({ + id: "name", + header: () => , + cell: ({ + row: { + original: { first_name, last_name }, + }, + }) => , + }), + columnHelper.accessor("has_account", { + header: () => , + cell: ({ getValue }) => , + }), + columnHelper.accessor("created_at", { + header: () => , + cell: ({ getValue }) => , + }), + ], + [] + ) +} diff --git a/packages/admin-next/dashboard/src/hooks/table/columns/use-product-table-columns.tsx b/packages/admin-next/dashboard/src/hooks/table/columns/use-product-table-columns.tsx new file mode 100644 index 0000000000..f8e9903209 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/columns/use-product-table-columns.tsx @@ -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() + +export const useProductTableColumns = () => { + return useMemo( + () => [ + columnHelper.display({ + id: "product", + header: () => , + cell: ({ row }) => , + }), + columnHelper.accessor("collection", { + header: () => , + cell: ({ row }) => ( + + ), + }), + columnHelper.accessor("sales_channels", { + header: () => , + cell: ({ row }) => ( + + ), + }), + columnHelper.accessor("variants", { + header: () => , + cell: ({ row }) => , + }), + columnHelper.accessor("status", { + header: () => , + cell: ({ row }) => , + }), + ], + [] + ) as ColumnDef[] +} diff --git a/packages/admin-next/dashboard/src/hooks/table/filters/use-customer-table-filters.tsx b/packages/admin-next/dashboard/src/hooks/table/filters/use-customer-table-filters.tsx new file mode 100644 index 0000000000..0e4c8c0958 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/filters/use-customer-table-filters.tsx @@ -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 +} diff --git a/packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx b/packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx new file mode 100644 index 0000000000..df9b58cb36 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/filters/use-product-table-filters.tsx @@ -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 +} diff --git a/packages/admin-next/dashboard/src/hooks/table/query/use-customer-table-query.tsx b/packages/admin-next/dashboard/src/hooks/table/query/use-customer-table-query.tsx new file mode 100644 index 0000000000..513cab37c2 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/query/use-customer-table-query.tsx @@ -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, + } +} diff --git a/packages/admin-next/dashboard/src/hooks/table/query/use-order-table-query.tsx b/packages/admin-next/dashboard/src/hooks/table/query/use-order-table-query.tsx index bcf52dad14..11348d6c2d 100644 --- a/packages/admin-next/dashboard/src/hooks/table/query/use-order-table-query.tsx +++ b/packages/admin-next/dashboard/src/hooks/table/query/use-order-table-query.tsx @@ -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 ) diff --git a/packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx b/packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx new file mode 100644 index 0000000000..5e0720dda5 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/query/use-product-table-query.tsx @@ -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, + } +} diff --git a/packages/admin-next/dashboard/src/hooks/use-data-table.tsx b/packages/admin-next/dashboard/src/hooks/use-data-table.tsx index 5be85018f4..ac8cb22a4f 100644 --- a/packages/admin-next/dashboard/src/hooks/use-data-table.tsx +++ b/packages/admin-next/dashboard/src/hooks/use-data-table.tsx @@ -10,27 +10,29 @@ import { import { useEffect, useMemo, useState } from "react" import { useSearchParams } from "react-router-dom" -type UseDataTableProps = { +type UseDataTableProps = { data?: TData[] - columns: ColumnDef[] + columns: ColumnDef[] count?: number pageSize?: number enableRowSelection?: boolean | ((row: Row) => boolean) enablePagination?: boolean getRowId?: (original: TData, index: number) => string + meta?: Record prefix?: string } -export const useDataTable = ({ +export const useDataTable = ({ data = [], columns, count = 0, - pageSize: _pageSize = 50, + pageSize: _pageSize = 20, enablePagination = true, enableRowSelection = false, getRowId, + meta, prefix, -}: UseDataTableProps) => { +}: UseDataTableProps) => { const [searchParams, setSearchParams] = useSearchParams() const offsetKey = `${prefix ? `${prefix}_` : ""}offset` const offset = searchParams.get(offsetKey) @@ -106,6 +108,7 @@ export const useDataTable = ({ ? getPaginationRowModel() : undefined, manualPagination: enablePagination ? true : undefined, + meta, }) return { table } diff --git a/packages/admin-next/dashboard/src/hooks/use-form-prompt.tsx b/packages/admin-next/dashboard/src/hooks/use-form-prompt.tsx index 0a7ff96102..206df07caf 100644 --- a/packages/admin-next/dashboard/src/hooks/use-form-prompt.tsx +++ b/packages/admin-next/dashboard/src/hooks/use-form-prompt.tsx @@ -8,8 +8,8 @@ export const useFormPrompt = () => { const promptValues = { title: t("general.unsavedChangesTitle"), description: t("general.unsavedChangesDescription"), - cancelText: t("general.cancel"), - confirmText: t("general.continue"), + cancelText: t("actions.cancel"), + confirmText: t("actions.continue"), } const prompt = async () => { diff --git a/packages/admin-next/dashboard/src/hooks/use-route-modal-state.tsx b/packages/admin-next/dashboard/src/hooks/use-route-modal-state.tsx index ce20816951..4348428819 100644 --- a/packages/admin-next/dashboard/src/hooks/use-route-modal-state.tsx +++ b/packages/admin-next/dashboard/src/hooks/use-route-modal-state.tsx @@ -3,13 +3,6 @@ import { useEffect, useState } from "react" import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" -type Prompt = { - title: string - description: string - cancelText: string - confirmText: string -} - /** * Hook for managing the state of route modals. */ @@ -30,11 +23,12 @@ export const useRouteModalState = (): [ const prompt = usePrompt() const { t } = useTranslation() - let promptValues: Prompt = { + const promptValues = { title: t("general.unsavedChangesTitle"), description: t("general.unsavedChangesDescription"), - cancelText: t("general.cancel"), - confirmText: t("general.continue"), + cancelText: t("actions.cancel"), + confirmText: t("actions.continue"), + variant: "confirmation" as const, } useEffect(() => { diff --git a/packages/admin-next/dashboard/src/i18n/config.ts b/packages/admin-next/dashboard/src/i18n/config.ts index ff128d9e5e..f8bdd59d9e 100644 --- a/packages/admin-next/dashboard/src/i18n/config.ts +++ b/packages/admin-next/dashboard/src/i18n/config.ts @@ -6,22 +6,27 @@ import { initReactI18next } from "react-i18next" import { Language } from "./types" -i18n +void i18n .use(Backend) .use(LanguageDetector) .use(initReactI18next) .init({ - fallbackLng: "en", + fallbackLng: "en-US", + load: "languageOnly", debug: process.env.NODE_ENV === "development", interpolation: { escapeValue: false, }, + backend: { + // for all available options read the backend's repository readme file + loadPath: "/locales/{{lng}}/{{ns}}.json", + }, }) export const languages: Language[] = [ { - code: "en", - display_name: "English", + code: "en-US", + display_name: "English (US)", ltr: true, date_locale: enUS, }, diff --git a/packages/admin-next/dashboard/src/i18n/types.ts b/packages/admin-next/dashboard/src/i18n/types.ts index 14e59028fd..35ebd4b677 100644 --- a/packages/admin-next/dashboard/src/i18n/types.ts +++ b/packages/admin-next/dashboard/src/i18n/types.ts @@ -1,5 +1,5 @@ import type { Locale } from "date-fns" -import en from "../../public/locales/en/translation.json" +import en from "../../public/locales/en-US/translation.json" const resources = { translation: en, diff --git a/packages/admin-next/dashboard/src/i18next.d.ts b/packages/admin-next/dashboard/src/i18next.d.ts index bd7cf8079a..e504f2f99c 100644 --- a/packages/admin-next/dashboard/src/i18next.d.ts +++ b/packages/admin-next/dashboard/src/i18next.d.ts @@ -1,7 +1,7 @@ -import { Resources } from "./i18n/types"; +import { Resources } from "./i18n/types" declare module "i18next" { interface CustomTypeOptions { - resources: Resources; + resources: Resources } } diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index d0e47ebaa5..43ac0c9c2d 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -2,6 +2,7 @@ import type { AdminCollectionsRes, AdminCustomerGroupsRes, AdminCustomersRes, + AdminGiftCardsRes, AdminProductsRes, AdminPublishableApiKeysRes, AdminRegionsRes, @@ -273,12 +274,29 @@ const router = createBrowserRouter([ }, children: [ { - index: true, - lazy: () => import("../../routes/gift-cards/list"), + path: "", + lazy: () => import("../../routes/gift-cards/gift-card-list"), + children: [ + { + path: "create", + lazy: () => + import("../../routes/gift-cards/gift-card-create"), + }, + ], }, { path: ":id", - lazy: () => import("../../routes/gift-cards/details"), + lazy: () => import("../../routes/gift-cards/gift-card-detail"), + handle: { + crumb: (data: AdminGiftCardsRes) => data.gift_card.code, + }, + children: [ + { + path: "edit", + lazy: () => + import("../../routes/gift-cards/gift-card-edit"), + }, + ], }, ], }, diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-add-sales-channels/components/add-sales-channels-to-api-key-form.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-add-sales-channels/components/add-sales-channels-to-api-key-form.tsx index 902dee4fc9..b20a33eb06 100644 --- a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-add-sales-channels/components/add-sales-channels-to-api-key-form.tsx +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-add-sales-channels/components/add-sales-channels-to-api-key-form.tsx @@ -156,16 +156,16 @@ export const AddSalesChannelsToApiKeyForm = ({ )}
    - -
    + +
    diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx index d7766fed21..7676ee4f0b 100644 --- a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-create/components/create-publishable-api-key-form/create-publishable-api-key-form.tsx @@ -57,14 +57,14 @@ export const CreatePublishableApiKeyForm = ({ onSubmit={handleSubmit} > -
    +
    diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx index 89ebed001d..ce320b4f5e 100644 --- a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-general-section/api-key-general-section.tsx @@ -39,8 +39,8 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => { description: t("apiKeyManagement.deleteKeyWarning", { title: apiKey.title, }), - confirmText: t("general.delete"), - cancelText: t("general.cancel"), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), }) if (!res) { @@ -57,7 +57,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => { title: apiKey.title, }), confirmText: t("apiKeyManagement.revoke"), - cancelText: t("general.cancel"), + cancelText: t("actions.cancel"), }) if (!res) { @@ -70,7 +70,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => { const dangerousActions = [ { icon: , - label: t("general.delete"), + label: t("actions.delete"), onClick: handleDelete, }, ] @@ -84,8 +84,8 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => { } return ( - -
    + +
    {apiKey.title}
    @@ -96,7 +96,7 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => { { actions: [ { - label: t("general.edit"), + label: t("actions.edit"), icon: , to: `/settings/api-key-management/${apiKey.id}/edit`, }, @@ -109,11 +109,11 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => { />
    -
    +
    {t("fields.key")} -
    +
    {apiKey.id} @@ -124,14 +124,14 @@ export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => { />
    -
    +
    {t("apiKeyManagement.createdBy")}
    {apiKey.revoked_at && ( -
    +
    {t("apiKeyManagement.revokedBy")} @@ -162,8 +162,8 @@ const ActionBy = ({ userId }: { userId: string | null }) => { if (isLoading) { return (
    - - + +
    ) } diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx index 3071e1e3f5..8faf515508 100644 --- a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-detail/components/api-key-sales-channel-section/api-key-sales-channel-section.tsx @@ -109,8 +109,8 @@ export const ApiKeySalesChannelSection = ({ description: t("apiKeyManagement.removeSalesChannelsWarning", { count: keys.length, }), - confirmText: t("general.continue"), - cancelText: t("general.cancel"), + confirmText: t("actions.continue"), + cancelText: t("actions.cancel"), }) if (!res) { @@ -136,15 +136,15 @@ export const ApiKeySalesChannelSection = ({ } return ( - -
    + +
    {t("salesChannels.domain")}
    {!noRecords && ( -
    +
    @@ -162,7 +162,7 @@ export const ApiKeySalesChannelSection = ({ return ( {headerGroup.headers.map((header) => { return ( @@ -229,7 +229,7 @@ export const ApiKeySalesChannelSection = ({ @@ -255,8 +255,8 @@ const SalesChannelActions = ({ const res = await prompt({ title: t("general.areYouSure"), description: t("apiKeyManagement.removeSalesChannelWarning"), - confirmText: t("general.delete"), - cancelText: t("general.cancel"), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), }) if (!res) { @@ -275,7 +275,7 @@ const SalesChannelActions = ({ actions: [ { icon: , - label: t("general.edit"), + label: t("actions.edit"), to: `/settings/sales-channels/${salesChannel.id}/edit`, }, ], @@ -284,7 +284,7 @@ const SalesChannelActions = ({ actions: [ { icon: , - label: t("general.delete"), + label: t("actions.delete"), onClick: handleDelete, }, ], diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/components/edit-api-key-form/edit-api-key-form.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/components/edit-api-key-form/edit-api-key-form.tsx index 0a061c564c..240c79cf63 100644 --- a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/components/edit-api-key-form/edit-api-key-form.tsx +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-edit/components/edit-api-key-form/edit-api-key-form.tsx @@ -76,11 +76,11 @@ export const EditApiKeyForm = ({
    diff --git a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/api-key-management-list.tsx b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/api-key-management-list.tsx index 9f65eeb782..b20cc90563 100644 --- a/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/api-key-management-list.tsx +++ b/packages/admin-next/dashboard/src/routes/api-key-management/api-key-management-list/api-key-management-list.tsx @@ -161,7 +161,7 @@ const CreatePublishableApiKey = (props: CreatePublishableApiKeyProps) => {
    @@ -186,8 +186,8 @@ const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => { description: t("apiKeyManagement.deleteKeyWarning", { title: apiKey.title, }), - confirmText: t("general.delete"), - cancelText: t("general.cancel"), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), }) if (!res) { @@ -204,7 +204,7 @@ const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => { title: apiKey.title, }), confirmText: t("apiKeyManagement.revoke"), - cancelText: t("general.cancel"), + cancelText: t("actions.cancel"), }) if (!res) { @@ -221,7 +221,7 @@ const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => { actions: [ { icon: , - label: t("general.edit"), + label: t("actions.edit"), to: `/settings/api-key-management/${apiKey.id}`, }, ], @@ -235,7 +235,7 @@ const KeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => { }, { icon: , - label: t("general.delete"), + label: t("actions.delete"), onClick: handleDelete, }, ], @@ -263,7 +263,7 @@ const useColumns = () => { return (
    e.stopPropagation()} > diff --git a/packages/admin-next/dashboard/src/routes/collections/collection-add-products/components/add-products-to-collection-form/add-products-to-collection-form.tsx b/packages/admin-next/dashboard/src/routes/collections/collection-add-products/components/add-products-to-collection-form/add-products-to-collection-form.tsx index ff7576e8c9..716bd85d8c 100644 --- a/packages/admin-next/dashboard/src/routes/collections/collection-add-products/components/add-products-to-collection-form/add-products-to-collection-form.tsx +++ b/packages/admin-next/dashboard/src/routes/collections/collection-add-products/components/add-products-to-collection-form/add-products-to-collection-form.tsx @@ -183,17 +183,17 @@ export const AddProductsToCollectionForm = ({ )}
    - + {!noRecords && ( -
    +
    @@ -209,7 +209,7 @@ export const AddProductsToCollectionForm = ({ onScroll={handleScroll} > {!isLoading && !products?.length ? ( -
    +
    ) : ( @@ -286,7 +286,7 @@ export const AddProductsToCollectionForm = ({
    ) : ( -
    +
    {/* TODO: fix this, and add NoRecords as well */}
    diff --git a/packages/admin-next/dashboard/src/routes/collections/collection-create/components/create-collection-form/create-collection-form.tsx b/packages/admin-next/dashboard/src/routes/collections/collection-create/components/create-collection-form/create-collection-form.tsx index 78b50dcad4..2b4146f297 100644 --- a/packages/admin-next/dashboard/src/routes/collections/collection-create/components/create-collection-form/create-collection-form.tsx +++ b/packages/admin-next/dashboard/src/routes/collections/collection-create/components/create-collection-form/create-collection-form.tsx @@ -56,7 +56,7 @@ export const CreateCollectionForm = ({
    -
    +
    {t("collections.createCollection")} @@ -107,7 +107,7 @@ export const CreateCollectionForm = ({
    -
    +
    +
    {collection.title} , - label: t("general.edit"), + label: t("actions.edit"), to: `/collections/${collection.id}/edit`, }, ], @@ -51,7 +51,7 @@ export const CollectionGeneralSection = ({ actions: [ { icon: , - label: t("general.delete"), + label: t("actions.delete"), onClick: handleDelete, }, ], @@ -59,18 +59,12 @@ export const CollectionGeneralSection = ({ ]} />
    -
    +
    {t("fields.handle")} /{collection.handle}
    -
    - - {t("fields.products")} - - {collection.products?.length || "-"} -
    ) } diff --git a/packages/admin-next/dashboard/src/routes/collections/collection-detail/components/collection-product-section/collection-product-section.tsx b/packages/admin-next/dashboard/src/routes/collections/collection-detail/components/collection-product-section/collection-product-section.tsx index fd0edc6a4b..a58d79c012 100644 --- a/packages/admin-next/dashboard/src/routes/collections/collection-detail/components/collection-product-section/collection-product-section.tsx +++ b/packages/admin-next/dashboard/src/routes/collections/collection-detail/components/collection-product-section/collection-product-section.tsx @@ -1,47 +1,21 @@ import { PencilSquare, Trash } from "@medusajs/icons" import type { Product, ProductCollection } from "@medusajs/medusa" -import { - Button, - Checkbox, - CommandBar, - Container, - Heading, - Table, - clx, - usePrompt, -} from "@medusajs/ui" -import { - PaginationState, - RowSelectionState, - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table" +import { Button, Checkbox, Container, Heading, usePrompt } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" import { adminProductKeys, useAdminProducts, useAdminRemoveProductsFromCollection, } from "medusa-react" -import { useMemo, useState } from "react" +import { useMemo } from "react" import { useTranslation } from "react-i18next" -import { Link, useNavigate } from "react-router-dom" +import { Link } from "react-router-dom" import { ActionMenu } from "../../../../../components/common/action-menu" -import { - NoRecords, - NoResults, -} from "../../../../../components/common/empty-table-content" -import { - ProductAvailabilityCell, - ProductCollectionCell, - ProductStatusCell, - ProductTitleCell, - ProductVariantCell, -} from "../../../../../components/common/product-table-cells" -import { OrderBy } from "../../../../../components/filtering/order-by" -import { Query } from "../../../../../components/filtering/query" -import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" -import { useQueryParams } from "../../../../../hooks/use-query-params" +import { DataTable } from "../../../../../components/table/data-table" +import { useProductTableColumns } from "../../../../../hooks/table/columns/use-product-table-columns" +import { useProductTableFilters } from "../../../../../hooks/table/filters/use-product-table-filters" +import { useProductTableQuery } from "../../../../../hooks/table/query/use-product-table-query" +import { useDataTable } from "../../../../../hooks/use-data-table" import { queryClient } from "../../../../../lib/medusa" type CollectionProductSectionProps = { @@ -54,51 +28,30 @@ export const CollectionProductSection = ({ collection, }: CollectionProductSectionProps) => { const { t } = useTranslation() - const navigate = useNavigate() - const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, - pageSize: PAGE_SIZE, - }) - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize] - ) - - const [rowSelection, setRowSelection] = useState({}) - - const params = useQueryParams(["q", "order"]) + const { searchParams, raw } = useProductTableQuery({ pageSize: PAGE_SIZE }) const { products, count, isLoading, isError, error } = useAdminProducts( { limit: PAGE_SIZE, - offset: pageIndex * PAGE_SIZE, + ...searchParams, collection_id: [collection.id], - ...params, }, { keepPreviousData: true, } ) + const filters = useProductTableFilters(["collections"]) const columns = useColumns() - const table = useReactTable({ + const { table } = useDataTable({ data: (products ?? []) as Product[], columns, - pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), - state: { - pagination, - rowSelection, - }, getRowId: (row) => row.id, - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, + count, + enablePagination: true, + enableRowSelection: true, + pageSize: PAGE_SIZE, meta: { collectionId: collection.id, }, @@ -107,16 +60,16 @@ export const CollectionProductSection = ({ const prompt = usePrompt() const { mutateAsync } = useAdminRemoveProductsFromCollection(collection.id) - const handleRemove = async () => { - const ids = Object.keys(rowSelection) + const handleRemove = async (selection: Record) => { + const ids = Object.keys(selection) const res = await prompt({ title: t("general.areYouSure"), description: t("collections.removeProductsWarning", { count: ids.length, }), - confirmText: t("general.confirm"), - cancelText: t("general.cancel"), + confirmText: t("actions.remove"), + cancelText: t("actions.cancel"), }) if (!res) { @@ -130,23 +83,17 @@ export const CollectionProductSection = ({ { onSuccess: () => { queryClient.invalidateQueries(adminProductKeys.lists()) - setRowSelection({}) }, } ) } - const noRecords = - !isLoading && - products?.length === 0 && - !Object.values(params).filter((v) => v).length - if (isError) { throw error } return ( - +
    {t("products.domain")} @@ -155,99 +102,26 @@ export const CollectionProductSection = ({
    - {!noRecords && ( -
    -
    -
    - - -
    -
    - )} - {noRecords ? ( - - ) : ( -
    - {!isLoading && !products?.length ? ( -
    - -
    - ) : ( - - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - navigate(`/products/${row.original.id}`)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
    - )} - - - - - {t("general.countSelected", { - count: Object.keys(rowSelection).length, - })} - - - - - -
    - )} + `/products/${original.id}`} + count={count} + filters={filters} + isLoading={isLoading} + orderBy={["title", "created_at", "updated_at"]} + queryObject={raw} + commands={[ + { + action: handleRemove, + label: t("actions.remove"), + shortcut: "r", + }, + ]} + />
    ) } @@ -269,8 +143,8 @@ const ProductActions = ({ description: t("collections.removeSingleProductWarning", { title: product.title, }), - confirmText: t("general.confirm"), - cancelText: t("general.cancel"), + confirmText: t("actions.remove"), + cancelText: t("actions.cancel"), }) if (!res) { @@ -289,7 +163,7 @@ const ProductActions = ({ actions: [ { icon: , - label: t("general.edit"), + label: t("actions.edit"), to: `/products/${product.id}/edit`, }, ], @@ -298,7 +172,7 @@ const ProductActions = ({ actions: [ { icon: , - label: t("general.remove"), + label: t("actions.remove"), onClick: handleRemove, }, ], @@ -311,7 +185,7 @@ const ProductActions = ({ const columnHelper = createColumnHelper() const useColumns = () => { - const { t } = useTranslation() + const columns = useProductTableColumns() return useMemo( () => [ @@ -343,44 +217,7 @@ const useColumns = () => { ) }, }), - columnHelper.accessor("title", { - header: t("fields.title"), - cell: ({ row }) => { - return - }, - }), - columnHelper.accessor("collection", { - header: t("fields.collection"), - cell: (cell) => { - const collection = cell.getValue() - - return - }, - }), - columnHelper.accessor("sales_channels", { - header: t("fields.availability"), - cell: (cell) => { - const salesChannels = cell.getValue() - - return - }, - }), - columnHelper.accessor("variants", { - header: t("fields.variants"), - cell: (cell) => { - const variants = cell.getValue() - - return - }, - }), - columnHelper.accessor("status", { - header: t("fields.status"), - cell: (cell) => { - const value = cell.getValue() - - return - }, - }), + ...columns, columnHelper.display({ id: "actions", cell: ({ row, table }) => { @@ -397,6 +234,6 @@ const useColumns = () => { }, }), ], - [t] + [columns] ) } diff --git a/packages/admin-next/dashboard/src/routes/collections/collection-edit/components/edit-collection-form/edit-collection-form.tsx b/packages/admin-next/dashboard/src/routes/collections/collection-edit/components/edit-collection-form/edit-collection-form.tsx index 3b720a8db3..44a56c986f 100644 --- a/packages/admin-next/dashboard/src/routes/collections/collection-edit/components/edit-collection-form/edit-collection-form.tsx +++ b/packages/admin-next/dashboard/src/routes/collections/collection-edit/components/edit-collection-form/edit-collection-form.tsx @@ -83,7 +83,7 @@ export const EditCollectionForm = ({
    -
    +
    diff --git a/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/collection-list-table.tsx b/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/collection-list-table.tsx index 700b92ef32..b68a115094 100644 --- a/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/collection-list-table.tsx +++ b/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/collection-list-table.tsx @@ -1,239 +1,66 @@ -import { ProductCollection } from "@medusajs/medusa" -import { Button, Container, Heading, Table, clx, usePrompt } from "@medusajs/ui" -import { - PaginationState, - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table" -import { useAdminCollections, useAdminDeleteCollection } from "medusa-react" -import { useMemo, useState } from "react" +import { Button, Container, Heading } from "@medusajs/ui" +import { useAdminCollections } from "medusa-react" import { useTranslation } from "react-i18next" -import { Link, useNavigate } from "react-router-dom" +import { Link } from "react-router-dom" -import { PencilSquare, Trash } from "@medusajs/icons" -import { ActionMenu } from "../../../../../components/common/action-menu" -import { - NoRecords, - NoResults, -} from "../../../../../components/common/empty-table-content" -import { Query } from "../../../../../components/filtering/query" -import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" -import { useQueryParams } from "../../../../../hooks/use-query-params" +import { DataTable } from "../../../../../components/table/data-table" +import { useDataTable } from "../../../../../hooks/use-data-table" +import { useCollectionTableColumns } from "./use-collection-table-columns" +import { useCollectionTableFilters } from "./use-collection-table-filters" +import { useCollectionTableQuery } from "./use-collection-table-query" -const PAGE_SIZE = 50 +const PAGE_SIZE = 20 export const CollectionListTable = () => { const { t } = useTranslation() - const navigate = useNavigate() - - const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, - pageSize: PAGE_SIZE, - }) - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize] - ) - - const params = useQueryParams(["q"]) + const { searchParams, raw } = useCollectionTableQuery({ pageSize: PAGE_SIZE }) const { collections, count, isError, error, isLoading } = useAdminCollections( { - limit: PAGE_SIZE, - offset: pageIndex * PAGE_SIZE, - ...params, + ...searchParams, }, { keepPreviousData: true, } ) - const columns = useColumns() + const filters = useCollectionTableFilters() + const columns = useCollectionTableColumns() - const table = useReactTable({ + const { table } = useDataTable({ data: collections ?? [], columns, - pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), - state: { - pagination, - }, - onPaginationChange: setPagination, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, }) - const noRecords = - !isLoading && - (!collections || collections.length === 0) && - !Object.values(params).filter(Boolean).length - if (isError) { throw error } return ( - +
    {t("collections.domain")}
    - {!noRecords && ( -
    -
    -
    - -
    -
    - )} - {noRecords ? ( - - ) : ( -
    - {!isLoading && !collections?.length ? ( -
    - -
    - ) : ( - - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - navigate(`/collections/${row.original.id}`)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
    - )} - -
    - )} + `/collections/${row.original.id}`} + queryObject={raw} + isLoading={isLoading} + />
    ) } - -const CollectionActions = ({ - collection, -}: { - collection: ProductCollection -}) => { - const { t } = useTranslation() - const prompt = usePrompt() - - const { mutateAsync } = useAdminDeleteCollection(collection.id) - - const handleDelete = async () => { - const res = await prompt({ - title: t("general.areYouSure"), - description: t("collections.deleteWarning", { - count: 1, - }), - }) - - if (!res) { - return - } - - await mutateAsync() - } - - return ( - , - label: t("general.edit"), - to: `/collections/${collection.id}/edit`, - }, - ], - }, - { - actions: [ - { - icon: , - label: t("general.delete"), - onClick: handleDelete, - }, - ], - }, - ]} - /> - ) -} - -const columnHelper = createColumnHelper() - -const useColumns = () => { - const { t } = useTranslation() - - return useMemo( - () => [ - columnHelper.accessor("title", { - header: t("fields.title"), - cell: ({ getValue }) => getValue(), - }), - columnHelper.display({ - id: "actions", - cell: ({ row }) => , - }), - ], - [t] - ) -} diff --git a/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-columns.tsx b/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-columns.tsx new file mode 100644 index 0000000000..39cea769ce --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-columns.tsx @@ -0,0 +1,94 @@ +import { PencilSquare, Trash } from "@medusajs/icons" +import { ProductCollection } from "@medusajs/medusa" +import { usePrompt } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useAdminDeleteCollection } from "medusa-react" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { ActionMenu } from "../../../../../components/common/action-menu" + +const columnHelper = createColumnHelper() + +export const useCollectionTableColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("title", { + header: t("fields.title"), + }), + columnHelper.accessor("handle", { + header: t("fields.handle"), + cell: ({ getValue }) => `/${getValue()}`, + }), + columnHelper.accessor("products", { + header: t("fields.products"), + cell: ({ getValue }) => { + const count = getValue()?.length + + return {count || "-"} + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} + +const CollectionActions = ({ + collection, +}: { + collection: ProductCollection +}) => { + const { t } = useTranslation() + const prompt = usePrompt() + + const { mutateAsync } = useAdminDeleteCollection(collection.id) + + const handleDeleteCollection = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("collections.deleteWarning", { + title: collection.title, + }), + verificationText: collection.title, + verificationInstruction: t("general.typeToConfirm"), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync() + } + + return ( + , + }, + ], + }, + { + actions: [ + { + label: t("actions.delete"), + onClick: handleDeleteCollection, + icon: , + }, + ], + }, + ]} + /> + ) +} diff --git a/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-filters.tsx b/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-filters.tsx new file mode 100644 index 0000000000..8dbb2d168c --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-filters.tsx @@ -0,0 +1,21 @@ +import { useTranslation } from "react-i18next" +import { Filter } from "../../../../../components/table/data-table" + +export const useCollectionTableFilters = () => { + const { t } = useTranslation() + + let filters: Filter[] = [] + + 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, ...dateFilters] + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-query.tsx b/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-query.tsx new file mode 100644 index 0000000000..5ddcfac4dd --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/collections/collection-list/components/collection-list-table/use-collection-table-query.tsx @@ -0,0 +1,33 @@ +import { AdminGetCollectionsParams } from "@medusajs/medusa" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +type UseCollectionTableQueryProps = { + prefix?: string + pageSize?: number +} + +export const useCollectionTableQuery = ({ + prefix, + pageSize = 20, +}: UseCollectionTableQueryProps) => { + const queryObject = useQueryParams( + ["offset", "q", "order", "created_at", "updated_at"], + prefix + ) + + const { offset, created_at, updated_at, q, order } = queryObject + + const searchParams: AdminGetCollectionsParams = { + limit: pageSize, + offset: offset ? Number(offset) : 0, + order, + created_at: created_at ? JSON.parse(created_at) : undefined, + updated_at: updated_at ? JSON.parse(updated_at) : undefined, + q, + } + + return { + searchParams, + raw: queryObject, + } +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/add-customers-form.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/add-customers-form.tsx index ac47e264f5..22e377f080 100644 --- a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/add-customers-form.tsx +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-add-customers/components/add-customers-form/add-customers-form.tsx @@ -161,7 +161,7 @@ export const AddCustomersForm = ({ )}
    - + {noRecords ? ( -
    +
    ) : ( -
    -
    +
    +
    @@ -242,7 +242,7 @@ export const AddCustomersForm = ({ ) : ( -
    +
    )} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/create-customer-group-form.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/create-customer-group-form.tsx index 76ff4075f0..f350be2964 100644 --- a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/create-customer-group-form.tsx +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-create/components/create-customer-group-form/create-customer-group-form.tsx @@ -56,10 +56,10 @@ export const CreateCustomerGroupForm = ({
    -
    +
    -
    +
    {t("customerGroups.createCustomerGroup")} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/customer-group-customer-section.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/customer-group-customer-section.tsx index ff21d6c43c..b1db06b4d0 100644 --- a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/customer-group-customer-section.tsx +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-customer-section/customer-group-customer-section.tsx @@ -1,39 +1,20 @@ import { PencilSquare, Trash } from "@medusajs/icons" import { Customer, CustomerGroup } from "@medusajs/medusa" -import { - Button, - Checkbox, - CommandBar, - Container, - Heading, - StatusBadge, - Table, - clx, - usePrompt, -} from "@medusajs/ui" -import { - PaginationState, - RowSelectionState, - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table" +import { Button, Checkbox, Container, Heading, usePrompt } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" import { useAdminCustomerGroupCustomers, useAdminRemoveCustomersFromCustomerGroup, } from "medusa-react" -import { useMemo, useState } from "react" +import { useMemo } from "react" import { useTranslation } from "react-i18next" -import { Link, useNavigate } from "react-router-dom" +import { Link } from "react-router-dom" import { ActionMenu } from "../../../../../components/common/action-menu" -import { - NoRecords, - NoResults, -} from "../../../../../components/common/empty-table-content" -import { Query } from "../../../../../components/filtering/query" -import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" -import { useQueryParams } from "../../../../../hooks/use-query-params" +import { DataTable } from "../../../../../components/table/data-table" +import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns" +import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters" +import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query" +import { useDataTable } from "../../../../../hooks/use-data-table" type CustomerGroupCustomerSectionProps = { group: CustomerGroup @@ -44,53 +25,31 @@ const PAGE_SIZE = 10 export const CustomerGroupCustomerSection = ({ group, }: CustomerGroupCustomerSectionProps) => { - const navigate = useNavigate() const { t } = useTranslation() - const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, - pageSize: PAGE_SIZE, - }) - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize] - ) - - const [rowSelection, setRowSelection] = useState({}) - - const params = useQueryParams(["q"]) + const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE }) const { customers, count, isLoading, isError, error } = useAdminCustomerGroupCustomers( group.id, { - limit: PAGE_SIZE, - offset: pageIndex * PAGE_SIZE, - ...params, + ...searchParams, }, { keepPreviousData: true, } ) + const filters = useCustomerTableFilters(["groups"]) const columns = useColumns() - const table = useReactTable({ + const { table } = useDataTable({ data: customers ?? [], columns, - pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), - state: { - pagination, - rowSelection, - }, + count, getRowId: (row) => row.id, - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, + enablePagination: true, + enableRowSelection: true, + pageSize: PAGE_SIZE, meta: { customerGroupId: group.id, }, @@ -99,46 +58,34 @@ export const CustomerGroupCustomerSection = ({ const { mutateAsync } = useAdminRemoveCustomersFromCustomerGroup(group.id) const prompt = usePrompt() - const handleRemoveCustomers = async () => { - const selected = Object.keys(rowSelection).filter((k) => rowSelection[k]) + const handleRemoveCustomers = async (selection: Record) => { + const selected = Object.keys(selection).filter((k) => selection[k]) const res = await prompt({ title: t("general.areYouSure"), description: t("customerGroups.removeCustomersWarning", { count: selected.length, }), - confirmText: t("general.continue"), - cancelText: t("general.cancel"), + confirmText: t("actions.continue"), + cancelText: t("actions.cancel"), }) if (!res) { return } - await mutateAsync( - { - customer_ids: selected.map((s) => ({ id: s })), - }, - { - onSuccess: () => { - setRowSelection({}) - }, - } - ) + await mutateAsync({ + customer_ids: selected.map((s) => ({ id: s })), + }) } - const noRecords = - !isLoading && - !customers?.length && - !Object.values(params).filter(Boolean).length - if (isError) { throw error } return ( - -
    + +
    {t("customers.domain")}
    -
    - {noRecords ? ( - - ) : ( -
    -
    -
    -
    - -
    -
    -
    - {(customers?.length || 0) > 0 ? ( - - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - - navigate(`/customers/${row.original.id}`) - } - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
    - ) : ( -
    - -
    - )} - - - - - {t("general.countSelected", { - count: Object.keys(rowSelection).length, - })} - - - - - -
    -
    - )} -
    + `/customers/${row.id}`} + filters={filters} + search + pagination + orderBy={[ + "email", + "first_name", + "last_name", + "has_account", + "created_at", + "updated_at", + ]} + commands={[ + { + action: handleRemoveCustomers, + label: t("actions.remove"), + shortcut: "r", + }, + ]} + queryObject={raw} + />
    ) } @@ -265,8 +143,8 @@ const CustomerActions = ({ description: t("customerGroups.removeCustomersWarning", { count: 1, }), - confirmText: t("general.continue"), - cancelText: t("general.cancel"), + confirmText: t("actions.continue"), + cancelText: t("actions.cancel"), }) if (!res) { @@ -285,7 +163,7 @@ const CustomerActions = ({ actions: [ { icon: , - label: t("general.edit"), + label: t("actions.edit"), to: `/customers/${customer.id}/edit`, }, ], @@ -294,7 +172,7 @@ const CustomerActions = ({ actions: [ { icon: , - label: t("general.remove"), + label: t("actions.remove"), onClick: handleRemove, }, ], @@ -307,7 +185,7 @@ const CustomerActions = ({ const columnHelper = createColumnHelper() const useColumns = () => { - const { t } = useTranslation() + const columns = useCustomerTableColumns() return useMemo( () => [ @@ -339,33 +217,7 @@ const useColumns = () => { ) }, }), - columnHelper.accessor("email", { - header: t("fields.email"), - cell: ({ getValue }) => {getValue()}, - }), - columnHelper.display({ - id: "name", - header: t("fields.name"), - cell: ({ row }) => { - const name = [row.original.first_name, row.original.last_name] - .filter(Boolean) - .join(" ") - - return name || "-" - }, - }), - columnHelper.accessor("has_account", { - header: t("fields.account"), - cell: ({ getValue }) => { - const hasAccount = getValue() - - return ( - - {hasAccount ? t("customers.registered") : t("customers.guest")} - - ) - }, - }), + ...columns, columnHelper.display({ id: "actions", cell: ({ row, table }) => { @@ -382,6 +234,6 @@ const useColumns = () => { }, }), ], - [t] + [columns] ) } diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/customer-group-general-section.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/customer-group-general-section.tsx index 8168b467c7..e86b466a67 100644 --- a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/customer-group-general-section.tsx +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-detail/components/customer-group-general-section/customer-group-general-section.tsx @@ -27,7 +27,7 @@ export const CustomerGroupGeneralSection = ({ } return ( - + {group.name} , - label: t("general.edit"), + label: t("actions.edit"), to: `/customer-groups/${group.id}/edit`, }, ], @@ -44,7 +44,7 @@ export const CustomerGroupGeneralSection = ({ actions: [ { icon: , - label: t("general.delete"), + label: t("actions.delete"), onClick: handleDelete, }, ], diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/edit-customer-group-form.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/edit-customer-group-form.tsx index 0e034ba603..0bcc1c4bcf 100644 --- a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/edit-customer-group-form.tsx +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-edit/components/edit-customer-group-form/edit-customer-group-form.tsx @@ -54,9 +54,9 @@ export const EditCustomerGroupForm = ({ - +
    diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/customer-group-list-table.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/customer-group-list-table.tsx index aa5c71bb73..6561d6c1f2 100644 --- a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/customer-group-list-table.tsx +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/customer-group-list-table.tsx @@ -1,251 +1,72 @@ -import { PencilSquare, Trash } from "@medusajs/icons" -import { CustomerGroup } from "@medusajs/medusa" -import { Button, Container, Heading, Table, clx, usePrompt } from "@medusajs/ui" -import { - PaginationState, - RowSelectionState, - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table" -import { - useAdminCustomerGroups, - useAdminDeleteCustomerGroup, -} from "medusa-react" -import { useMemo, useState } from "react" +import { Button, Container, Heading } from "@medusajs/ui" +import { useAdminCustomerGroups } from "medusa-react" import { useTranslation } from "react-i18next" -import { Link, useNavigate } from "react-router-dom" -import { ActionMenu } from "../../../../../components/common/action-menu" -import { - NoRecords, - NoResults, -} from "../../../../../components/common/empty-table-content" -import { OrderBy } from "../../../../../components/filtering/order-by" -import { Query } from "../../../../../components/filtering/query" -import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" -import { useQueryParams } from "../../../../../hooks/use-query-params" +import { Link } from "react-router-dom" +import { DataTable } from "../../../../../components/table/data-table" +import { useDataTable } from "../../../../../hooks/use-data-table" +import { useCustomerGroupTableColumns } from "./use-customer-group-table-columns" +import { useCustomerGroupTableFilters } from "./use-customer-group-table-filters" +import { useCustomerGroupTableQuery } from "./use-customer-group-table-query" -const PAGE_SIZE = 50 +const PAGE_SIZE = 20 export const CustomerGroupListTable = () => { - const navigate = useNavigate() const { t } = useTranslation() - const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, + const { searchParams, raw } = useCustomerGroupTableQuery({ pageSize: PAGE_SIZE, }) - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize] - ) - - const [rowSelection, setRowSelection] = useState({}) - - const params = useQueryParams(["q", "order"]) const { customer_groups, count, isLoading, isError, error } = - useAdminCustomerGroups({ - limit: PAGE_SIZE, - offset: pageIndex * PAGE_SIZE, - ...params, - }) + useAdminCustomerGroups( + { + ...searchParams, + expand: "customers", + fields: "id,name,customers,customers.id", + }, + { + keepPreviousData: true, + } + ) - const columns = useColumns() + const filters = useCustomerGroupTableFilters() + const columns = useCustomerGroupTableColumns() - const table = useReactTable({ + const { table } = useDataTable({ data: customer_groups ?? [], columns, - pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), - state: { - pagination, - rowSelection, - }, - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, + enablePagination: true, + count, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, }) - const noRecords = - !isLoading && - !customer_groups?.length && - !Object.values(params).filter(Boolean).length - if (isError) { throw error } return ( - +
    {t("customerGroups.domain")}
    -
    - {noRecords ? ( - - ) : ( -
    -
    -
    -
    - - -
    -
    -
    - {(customer_groups?.length || 0) > 0 ? ( - - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - - navigate(`/customer-groups/${row.original.id}`) - } - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
    - ) : ( -
    - -
    - )} - -
    -
    - )} -
    + `/customer-groups/${row.original.id}`} + orderBy={["name", "created_at", "updated_at"]} + queryObject={raw} + isLoading={isLoading} + />
    ) } - -const CustomerGroupActions = ({ group }: { group: CustomerGroup }) => { - const { t } = useTranslation() - const prompt = usePrompt() - - const { mutateAsync } = useAdminDeleteCustomerGroup(group.id) - - const handleDelete = async () => { - const res = await prompt({ - title: t("general.areYouSure"), - description: t("customerGroups.deleteCustomerGroupWarning", { - name: group.name, - }), - confirmText: t("general.delete"), - cancelText: t("general.cancel"), - }) - - if (!res) { - return - } - - await mutateAsync(undefined) - } - - return ( - , - label: t("general.edit"), - to: `/customer-groups/${group.id}/edit`, - }, - ], - }, - { - actions: [ - { - icon: , - label: t("general.delete"), - onClick: handleDelete, - }, - ], - }, - ]} - /> - ) -} - -const columnHelper = createColumnHelper() - -const useColumns = () => { - const { t } = useTranslation() - - return useMemo( - () => [ - columnHelper.accessor("name", { - header: t("fields.name"), - cell: ({ getValue }) => getValue(), - }), - columnHelper.display({ - id: "actions", - cell: ({ row }) => , - }), - ], - [t] - ) -} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-columns.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-columns.tsx new file mode 100644 index 0000000000..87b070b426 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-columns.tsx @@ -0,0 +1,85 @@ +import { PencilSquare, Trash } from "@medusajs/icons" +import { CustomerGroup } from "@medusajs/medusa" +import { usePrompt } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useAdminDeleteCustomerGroup } from "medusa-react" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { ActionMenu } from "../../../../../components/common/action-menu" + +const columnHelper = createColumnHelper() + +export const useCustomerGroupTableColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("name", { + header: t("fields.name"), + cell: ({ getValue }) => getValue(), + }), + columnHelper.accessor("customers", { + header: t("customers.domain"), + cell: ({ getValue }) => { + const count = getValue()?.length ?? 0 + + return count + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} + +const CustomerGroupActions = ({ group }: { group: CustomerGroup }) => { + const { t } = useTranslation() + const prompt = usePrompt() + + const { mutateAsync } = useAdminDeleteCustomerGroup(group.id) + + const handleDelete = async () => { + const res = await prompt({ + title: t("general.areYouSure"), + description: t("customerGroups.deleteCustomerGroupWarning", { + name: group.name, + }), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync() + } + + return ( + , + }, + ], + }, + { + actions: [ + { + label: t("actions.delete"), + onClick: handleDelete, + icon: , + }, + ], + }, + ]} + /> + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-filters.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-filters.tsx new file mode 100644 index 0000000000..8830b37a92 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-filters.tsx @@ -0,0 +1,21 @@ +import { useTranslation } from "react-i18next" +import { Filter } from "../../../../../components/table/data-table" + +export const useCustomerGroupTableFilters = () => { + const { t } = useTranslation() + + let filters: Filter[] = [] + + 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, ...dateFilters] + + return filters +} diff --git a/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-query.tsx b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-query.tsx new file mode 100644 index 0000000000..05c2275b69 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customer-groups/customer-group-list/components/customer-group-list-table/use-customer-group-table-query.tsx @@ -0,0 +1,33 @@ +import { AdminGetCustomerGroupsParams } from "@medusajs/medusa" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +type UseCustomerGroupTableQueryProps = { + prefix?: string + pageSize?: number +} + +export const useCustomerGroupTableQuery = ({ + prefix, + pageSize = 20, +}: UseCustomerGroupTableQueryProps) => { + const queryObject = useQueryParams( + ["offset", "q", "order", "created_at", "updated_at"], + prefix + ) + + const { offset, created_at, updated_at, q, order } = queryObject + + const searchParams: AdminGetCustomerGroupsParams = { + limit: pageSize, + offset: offset ? Number(offset) : 0, + order, + created_at: created_at ? JSON.parse(created_at) : undefined, + updated_at: updated_at ? JSON.parse(updated_at) : undefined, + q, + } + + return { + searchParams, + raw: queryObject, + } +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx index bbbc7898db..f0d7669d84 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx +++ b/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx @@ -80,7 +80,7 @@ export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => {
    -
    +
    {t("customers.createCustomer")} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx index 1fc3b5af2b..c5333c75cd 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx @@ -1,9 +1,8 @@ +import { PencilSquare } from "@medusajs/icons" import { Customer } from "@medusajs/medusa" -import { Button, Container, Heading, Text } from "@medusajs/ui" -import format from "date-fns/format" -import { ReactNode } from "react" +import { Container, Heading, StatusBadge, Text } from "@medusajs/ui" import { useTranslation } from "react-i18next" -import { Link } from "react-router-dom" +import { ActionMenu } from "../../../../../components/common/action-menu" type CustomerGeneralSectionProps = { customer: Customer @@ -14,60 +13,52 @@ export const CustomerGeneralSection = ({ }: CustomerGeneralSectionProps) => { const { t } = useTranslation() + const name = [customer.first_name, customer.last_name] + .filter(Boolean) + .join(" ") + + const statusColor = customer.has_account ? "green" : "orange" + const statusText = customer.has_account + ? t("customers.registered") + : t("customers.guest") + return ( - -
    + +
    {customer.email} - - - +
    + {statusText} + , + to: "edit", + }, + ], + }, + ]} + /> +
    -
    - - - - - +
    + + {t("fields.name")} + + + {name ?? "-"} + +
    +
    + + {t("fields.phone")} + + + {customer.phone ?? "-"} +
    ) } - -const Bulletpoint = ({ title, value }: { title: string; value: ReactNode }) => { - return ( -
    - - {title} - -
    {value ?? "-"}
    -
    - ) -} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx index a2f307c568..8c9d4e6f27 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx @@ -60,7 +60,7 @@ export const CustomerOrderSection = ({ {t("orders.domain")}
    diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx index 8b1cbc21d1..bf5e0baead 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx +++ b/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx @@ -135,7 +135,7 @@ export const EditCustomerForm = ({
    diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx index 2ffab56dc8..72816587c2 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx +++ b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx @@ -1,158 +1,79 @@ import { PencilSquare } from "@medusajs/icons" import { Customer } from "@medusajs/medusa" -import { - Button, - Container, - Heading, - StatusBadge, - Table, - clx, -} from "@medusajs/ui" -import { - PaginationState, - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table" +import { Button, Container, Heading } from "@medusajs/ui" +import { ColumnDef, createColumnHelper } from "@tanstack/react-table" import { useAdminCustomers } from "medusa-react" -import { useMemo, useState } from "react" +import { useMemo } from "react" import { useTranslation } from "react-i18next" -import { Link, useNavigate } from "react-router-dom" -import { ActionMenu } from "../../../../../components/common/action-menu" -import { NoRecords } from "../../../../../components/common/empty-table-content" -import { Query } from "../../../../../components/filtering/query" -import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" -import { useQueryParams } from "../../../../../hooks/use-query-params" +import { Link } from "react-router-dom" -const PAGE_SIZE = 50 +import { ActionMenu } from "../../../../../components/common/action-menu" +import { DataTable } from "../../../../../components/table/data-table" +import { useCustomerTableColumns } from "../../../../../hooks/table/columns/use-customer-table-columns" +import { useCustomerTableFilters } from "../../../../../hooks/table/filters/use-customer-table-filters" +import { useCustomerTableQuery } from "../../../../../hooks/table/query/use-customer-table-query" +import { useDataTable } from "../../../../../hooks/use-data-table" + +const PAGE_SIZE = 20 export const CustomerListTable = () => { const { t } = useTranslation() - const navigate = useNavigate() - const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, - pageSize: PAGE_SIZE, - }) - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize] + const { searchParams, raw } = useCustomerTableQuery({ pageSize: PAGE_SIZE }) + const { customers, count, isLoading, isError, error } = useAdminCustomers( + { + ...searchParams, + }, + { + keepPreviousData: true, + } ) - const params = useQueryParams(["q"]) - const { customers, count, isLoading, isError, error } = useAdminCustomers({ - limit: PAGE_SIZE, - offset: pageIndex * PAGE_SIZE, - ...params, - }) - + const filters = useCustomerTableFilters() const columns = useColumns() - const table = useReactTable({ + const { table } = useDataTable({ data: customers ?? [], columns, - pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), - state: { - pagination, - }, - onPaginationChange: setPagination, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, + count, + enablePagination: true, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, }) - const noRecords = - !isLoading && - (!customers || customers.length === 0) && - !Object.values(params).filter(Boolean).length - if (isError) { throw error } return ( - -
    + +
    {t("customers.domain")}
    -
    -
    -
    - -
    -
    - {noRecords ? ( - - ) : ( -
    - - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - navigate(`/customers/${row.original.id}`)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
    - -
    - )} + row.original.id} + search + queryObject={raw} + />
    ) } @@ -167,7 +88,7 @@ const CustomerActions = ({ customer }: { customer: Customer }) => { actions: [ { icon: , - label: t("general.edit"), + label: t("actions.edit"), to: `/customers/${customer.id}/edit`, }, ], @@ -180,42 +101,16 @@ const CustomerActions = ({ customer }: { customer: Customer }) => { const columnHelper = createColumnHelper() const useColumns = () => { - const { t } = useTranslation() + const columns = useCustomerTableColumns() return useMemo( () => [ - columnHelper.accessor("email", { - header: t("fields.email"), - cell: ({ getValue }) => {getValue()}, - }), - columnHelper.display({ - id: "name", - header: t("fields.name"), - cell: ({ row }) => { - const name = [row.original.first_name, row.original.last_name] - .filter(Boolean) - .join(" ") - - return name || "-" - }, - }), - columnHelper.accessor("has_account", { - header: t("fields.account"), - cell: ({ getValue }) => { - const hasAccount = getValue() - - return ( - - {hasAccount ? t("customers.registered") : t("customers.guest")} - - ) - }, - }), + ...columns, columnHelper.display({ id: "actions", cell: ({ row }) => , }), ], - [t] - ) + [columns] + ) as ColumnDef[] } diff --git a/packages/admin-next/dashboard/src/routes/gift-cards/details/details.tsx b/packages/admin-next/dashboard/src/routes/gift-cards/details/details.tsx deleted file mode 100644 index a3fb2af4c7..0000000000 --- a/packages/admin-next/dashboard/src/routes/gift-cards/details/details.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const GiftCardDetails = () => { - return
    Gift Card Details
    ; -}; diff --git a/packages/admin-next/dashboard/src/routes/gift-cards/details/index.ts b/packages/admin-next/dashboard/src/routes/gift-cards/details/index.ts deleted file mode 100644 index efdfebaf78..0000000000 --- a/packages/admin-next/dashboard/src/routes/gift-cards/details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { GiftCardDetails as Component } from "./details"; diff --git a/packages/admin-next/dashboard/src/routes/gift-cards/gift-card-create/components/create-gift-card-form/create-gift-card-form.tsx b/packages/admin-next/dashboard/src/routes/gift-cards/gift-card-create/components/create-gift-card-form/create-gift-card-form.tsx new file mode 100644 index 0000000000..af29abc6ac --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/gift-cards/gift-card-create/components/create-gift-card-form/create-gift-card-form.tsx @@ -0,0 +1,311 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { + Button, + CurrencyInput, + DatePicker, + FocusModal, + Heading, + Input, + Select, + Switch, + Text, + Textarea, + Tooltip, +} from "@medusajs/ui" +import * as Collapsible from "@radix-ui/react-collapsible" +import { useAdminCreateGiftCard, useAdminRegions } from "medusa-react" +import { useEffect, useState } from "react" +import { useForm, useWatch } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" + +import { useNavigate } from "react-router-dom" +import { Form } from "../../../../../components/common/form" +import { currencies } from "../../../../../lib/currencies" +import { getDbAmount } from "../../../../../lib/money-amount-helpers" + +type CreateGiftCardFormProps = { + subscribe: (state: boolean) => void +} + +const CreateGiftCardSchema = zod.object({ + region_id: zod.string(), + value: zod.string(), + is_enabled: zod.boolean(), + ends_at: zod.date().nullable(), + email: zod.string().email(), + personal_message: zod.string().optional(), +}) + +export const CreateGiftCardForm = ({ subscribe }: CreateGiftCardFormProps) => { + const [showDateFields, setShowDateFields] = useState(false) + + const { regions } = useAdminRegions({ + limit: 1000, + fields: "id,name,currency_code", + }) + const { t } = useTranslation() + const navigate = useNavigate() + + const form = useForm>({ + defaultValues: { + region_id: regions?.[0]?.id ?? "", + value: "", + is_enabled: true, + ends_at: null, + email: "", + personal_message: "", + }, + resolver: zodResolver(CreateGiftCardSchema), + }) + + const { + formState: { isDirty }, + setValue, + setError, + } = form + + const regionId = useWatch({ + control: form.control, + name: "region_id", + }) + + const currencyCode = regions?.find((r) => r.id === regionId)?.currency_code + const nativeSymbol = currencyCode + ? currencies[currencyCode.toUpperCase()].symbol_native + : undefined + + useEffect(() => { + subscribe(isDirty) + }, [isDirty, subscribe]) + + const { mutateAsync, isLoading } = useAdminCreateGiftCard() + + const handleOpenChange = (open: boolean) => { + if (!open) { + setValue("ends_at", null, { + shouldDirty: true, + }) + } + + setShowDateFields(open) + } + + const handleSubmit = form.handleSubmit(async (data) => { + if (!currencyCode) { + setError("region_id", { + type: "manual", + message: "Region not found", + }) + + return + } + + await mutateAsync( + { + region_id: data.region_id, + value: getDbAmount(parseFloat(data.value), currencyCode), + is_disabled: !data.is_enabled, + ends_at: data.ends_at ?? undefined, + metadata: { + email: data.email, + personal_message: data.personal_message, + }, + }, + { + onSuccess: ({ gift_card }) => { + navigate(`../${gift_card.id}`, { replace: true }) + }, + } + ) + }) + + return ( + + + +
    + + + + +
    +
    + +
    +
    + {t("giftCards.createGiftCard")} + + {t("giftCards.createGiftCardHint")} + +
    +
    + { + return ( + + {t("fields.region")} + + + + + + ) + }} + /> + { + return ( + + {t("giftCards.balance")} + + {!currencyCode || !nativeSymbol ? ( + + + + ) : ( + + )} + + + + ) + }} + /> +
    + { + return ( + +
    + {t("general.enabled")} + + + +
    + + {t("giftCards.enabledHint")} + + +
    + ) + }} + /> + { + return ( + + +
    +
    + + {t("fields.expiryDate")} + + +
    + + {t("giftCards.expiryDateHint")} + +
    + +
    + + { + onChange(v ?? null) + }} + {...field} + /> + +
    +
    +
    +
    + ) + }} + /> +
    +
    + { + return ( + + {t("fields.email")} + + + + + + ) + }} + /> +
    + { + return ( + + + {t("giftCards.personalMessage")} + + +