feat(admin-ui): Multi-language support (#4962)

This commit is contained in:
Geoffroy Empain
2023-09-12 14:53:48 +02:00
committed by GitHub
parent 107aaa371c
commit afd4e72cdf
348 changed files with 9668 additions and 2298 deletions
@@ -1,5 +1,6 @@
import clsx from "clsx"
import React, { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { sum } from "lodash"
import Tooltip from "../../../../components/atoms/tooltip"
@@ -57,6 +58,7 @@ function Input(props: InputProps) {
disabled,
} = props
const selectedCount = Object.keys(selected).length
const { t } = useTranslation()
const selectedOption = useMemo(() => {
const ret: string[] = []
@@ -100,7 +102,9 @@ function Input(props: InputProps) {
)}
{selectedCount === 0 ? (
<span className="text-grey-50">
{placeholder ? placeholder : "Choose categories"}
{placeholder
? placeholder
: t("multiselect-choose-categories", "Choose categories")}
</span>
) : null}
</div>
@@ -157,6 +161,7 @@ function PopupItem(props: PopupItemProps) {
selectedSubcategoriesCount,
} = props
const { t } = useTranslation()
const hasChildren = !!option.children?.length
const onClick = (e) => {
@@ -190,7 +195,11 @@ function PopupItem(props: PopupItemProps) {
<div className="flex items-center gap-2">
{!!selectedSubcategoriesCount && (
<span className="text-small text-gray-400">
{selectedSubcategoriesCount} selected
{t(
"domain-categories-multiselect-selected-with-counts",
"{{count}}",
{ count: selectedSubcategoriesCount }
)}
</span>
)}
<ChevronRightIcon size={16} />
@@ -5,6 +5,7 @@ import {
} from "medusa-react"
import { useEffect, useState } from "react"
import { useNavigate, useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
import Spacer from "../../../components/atoms/spacer"
import Spinner from "../../../components/atoms/spinner"
@@ -25,11 +26,13 @@ import { useWidgets } from "../../../providers/widget-provider"
import Medusa from "../../../services/api"
import { getErrorMessage } from "../../../utils/error-messages"
import { getErrorStatus } from "../../../utils/get-error-status"
import { useTranslation } from "react-i18next"
const CollectionDetails = () => {
const { id } = useParams()
const { collection, isLoading, error, refetch } = useAdminCollection(id!)
const { t } = useTranslation()
const deleteCollection = useAdminDeleteCollection(id!)
const updateCollection = useAdminUpdateCollection(id!)
const [showEdit, setShowEdit] = useState(false)
@@ -97,10 +100,17 @@ const CollectionDetails = () => {
}
setShowAddProducts(false)
notification("Success", "Updated products in collection", "success")
notification(
t("details-success", "Success"),
t(
"details-updated-products-in-collection",
"Updated products in collection"
),
"success"
)
refetch()
} catch (error) {
notification("Error", getErrorMessage(error), "error")
notification(t("details-error", "Error"), getErrorMessage(error), "error")
}
}
@@ -142,7 +152,7 @@ const CollectionDetails = () => {
<BackButton
className="mb-xsmall"
path="/a/products?view=collections"
label="Back to Collections"
label={t("details-back-to-collections", "Back to Collections")}
/>
<div className="gap-y-xsmall flex flex-col">
{getWidgets("product_collection.details.before").map((w, i) => {
@@ -167,12 +177,12 @@ const CollectionDetails = () => {
forceDropdown
actions={[
{
label: "Edit Collection",
label: t("details-edit-collection", "Edit Collection"),
onClick: () => setShowEdit(true),
icon: <EditIcon size="20" />,
},
{
label: "Delete",
label: t("details-delete", "Delete"),
onClick: () => setShowDelete(!showDelete),
variant: "danger",
icon: <TrashIcon size="20" />,
@@ -186,7 +196,9 @@ const CollectionDetails = () => {
</div>
{collection.metadata && (
<div className="mt-large gap-y-base flex flex-col">
<h3 className="inter-base-semibold">Metadata</h3>
<h3 className="inter-base-semibold">
{t("details-metadata", "Metadata")}
</h3>
<div>
<JSONView data={collection.metadata} />
</div>
@@ -199,14 +211,17 @@ const CollectionDetails = () => {
title="Products"
actions={[
{
label: "Edit Products",
label: t("details-edit-products", "Edit Products"),
icon: <EditIcon size="20" />,
onClick: () => setShowAddProducts(!showAddProducts),
},
]}
>
<p className="text-grey-50 inter-base-regular mt-xsmall mb-base">
Products in this collection
{t(
"details-products-in-this-collection",
"Products in this collection"
)}
</p>
{collection && (
<ViewProductsTable
@@ -228,7 +243,10 @@ const CollectionDetails = () => {
)
})}
<RawJSON data={collection} title="Raw collection" />
<RawJSON
data={collection}
title={t("details-raw-collection", "Raw collection")}
/>
</div>
<Spacer />
</div>
@@ -243,10 +261,13 @@ const CollectionDetails = () => {
{showDelete && (
<DeletePrompt
handleClose={() => setShowDelete(!showDelete)}
heading="Delete collection"
successText="Successfully deleted collection"
heading={t("details-delete-collection", "Delete collection")}
successText={t(
"details-successfully-deleted-collection",
"Successfully deleted collection"
)}
onDelete={async () => handleDelete()}
confirmText="Yes, delete"
confirmText={t("details-yes-delete", "Yes, delete")}
/>
)}
{showAddProducts && (
@@ -2,6 +2,7 @@ import { Customer } from "@medusajs/medusa"
import { useAdminUpdateCustomer } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import MetadataForm, {
getMetadataFormValues,
getSubmittableMetadata,
@@ -33,6 +34,8 @@ const EditCustomerModal = ({
handleClose,
customer,
}: EditCustomerModalProps) => {
const { t } = useTranslation()
const form = useForm<EditCustomerFormType>({
defaultValues: getDefaultValues(customer),
})
@@ -61,11 +64,22 @@ const EditCustomerModal = ({
{
onSuccess: () => {
handleClose()
notification("Success", "Successfully updated customer", "success")
notification(
t("details-success", "Success"),
t(
"details-successfully-updated-customer",
"Successfully updated customer"
),
"success"
)
},
onError: (err) => {
handleClose()
notification("Error", getErrorMessage(err), "error")
notification(
t("details-error", "Error"),
getErrorMessage(err),
"error"
)
},
}
)
@@ -79,22 +93,26 @@ const EditCustomerModal = ({
<Modal handleClose={handleClose}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<span className="inter-xlarge-semibold">Customer Details</span>
<span className="inter-xlarge-semibold">
{t("details-customer-details", "Customer Details")}
</span>
</Modal.Header>
<Modal.Content>
<div className="gap-y-xlarge flex flex-col">
<div>
<h2 className="inter-base-semibold text-grey-90 mb-4">General</h2>
<h2 className="inter-base-semibold text-grey-90 mb-4">
{t("details-general", "General")}
</h2>
<div className="flex w-full space-x-2">
<InputField
label="First Name"
label={t("details-first-name", "First Name")}
{...register("first_name")}
placeholder="Lebron"
placeholder={t("details-lebron", "Lebron")}
/>
<InputField
label="Last Name"
label={t("details-last-name", "Last Name")}
{...register("last_name")}
placeholder="James"
placeholder={t("details-james", "James")}
/>
</div>
</div>
@@ -102,7 +120,7 @@ const EditCustomerModal = ({
<h2 className="inter-base-semibold text-grey-90 mb-4">Contact</h2>
<div className="flex space-x-2">
<InputField
label="Email"
label={t("details-email", "Email")}
{...register("email", {
validate: (value) => !!validateEmail(value),
disabled: customer.has_account,
@@ -115,14 +133,16 @@ const EditCustomerModal = ({
disabled={customer.has_account}
/>
<InputField
label="Phone number"
label={t("details-phone-number", "Phone number")}
{...register("phone")}
placeholder="+45 42 42 42 42"
/>
</div>
</div>
<div>
<h2 className="inter-base-semibold mb-base">Metadata</h2>
<h2 className="inter-base-semibold mb-base">
{t("details-metadata", "Metadata")}
</h2>
<MetadataForm form={nestedForm(form, "metadata")} />
</div>
</div>
@@ -136,7 +156,7 @@ const EditCustomerModal = ({
className="mr-2"
type="button"
>
Cancel
{t("details-cancel", "Cancel")}
</Button>
<Button
loading={updateCustomer.isLoading}
@@ -145,7 +165,7 @@ const EditCustomerModal = ({
size="small"
onClick={onSubmit}
>
Save and close
{t("details-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>
@@ -2,6 +2,7 @@ import { useAdminCustomer } from "medusa-react"
import moment from "moment"
import { useState } from "react"
import { useNavigate, useParams } from "react-router-dom"
import { useTranslation } from "react-i18next"
import Avatar from "../../../components/atoms/avatar"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
@@ -24,6 +25,7 @@ const CustomerDetail = () => {
const navigate = useNavigate()
const { customer, isLoading, error } = useAdminCustomer(id!)
const { t } = useTranslation()
const [showEdit, setShowEdit] = useState(false)
const customerName = () => {
@@ -36,7 +38,7 @@ const CustomerDetail = () => {
const actions: ActionType[] = [
{
label: "Edit",
label: t("details-edit", "Edit"),
onClick: () => setShowEdit(true),
icon: <EditIcon size={20} />,
},
@@ -70,7 +72,7 @@ const CustomerDetail = () => {
return (
<div>
<BackButton
label="Back to Customers"
label={t("details-back-to-customers", "Back to Customers")}
path="/a/customers"
className="mb-xsmall"
/>
@@ -110,13 +112,13 @@ const CustomerDetail = () => {
<div className="mt-6 flex space-x-6 divide-x">
<div className="flex flex-col">
<div className="inter-smaller-regular text-grey-50 mb-1">
First seen
{t("details-first-seen", "First seen")}
</div>
<div>{moment(customer.created_at).format("DD MMM YYYY")}</div>
</div>
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Phone
{t("details-phone", "Phone")}
</div>
<div className="max-w-[200px] truncate">
{customer.phone || "N/A"}
@@ -124,13 +126,13 @@ const CustomerDetail = () => {
</div>
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Orders
{t("details-orders", "Orders")}
</div>
<div>{customer.orders.length}</div>
</div>
<div className="h-100 flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
User
{t("details-user", "User")}
</div>
<div className="h-50 flex items-center justify-center">
<StatusDot
@@ -142,8 +144,13 @@ const CustomerDetail = () => {
</div>
</Section>
<BodyCard
title={`Orders (${customer.orders.length})`}
subtitle="An overview of Customer Orders"
title={t("details-orders", "Orders {{count}}", {
count: customer.orders.length,
})}
subtitle={t(
"details-an-overview-of-customer-orders",
"An overview of Customer Orders"
)}
>
<div className="flex grow flex-col">
<CustomerOrdersTable id={customer.id} />
@@ -161,7 +168,10 @@ const CustomerDetail = () => {
)
})}
<RawJSON data={customer} title="Raw customer" />
<RawJSON
data={customer}
title={t("details-raw-customer", "Raw customer")}
/>
</div>
{showEdit && customer && (
@@ -20,6 +20,7 @@ import Modal from "../../../components/molecules/modal"
import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form"
import { useTranslation } from "react-i18next"
type CustomerGroupModalProps = {
open: boolean
@@ -40,6 +41,7 @@ function CustomerGroupModal({
onClose,
open,
}: CustomerGroupModalProps) {
const { t } = useTranslation()
const form = useForm<CustomerGroupModalFormType>({
defaultValues: getDefaultValues(customerGroup),
})
@@ -64,10 +66,18 @@ function CustomerGroupModal({
const { general, metadata } = data
const onSuccess = () => {
const title = customerGroup ? "Group Updated" : "Group Created"
const title = customerGroup
? t("groups-group-updated", "Group Updated")
: t("groups-group-created", "Group Created")
const msg = customerGroup
? "The customer group has been updated"
: "The customer group has been created"
? t(
"groups-the-customer-group-has-been-updated",
"The customer group has been updated"
)
: t(
"groups-the-customer-group-has-been-created",
"The customer group has been created"
)
notification(title, msg, "success")
@@ -110,7 +120,12 @@ function CustomerGroupModal({
<Modal.Body>
<Modal.Header handleClose={onClose}>
<span className="inter-xlarge-semibold">
{customerGroup ? "Edit" : "Create a New"} Customer Group
{customerGroup
? t("groups-edit-customer-group", "Edit Customer Group")
: t(
"groups-create-a-new-customer-group",
"Create a New Customer Group"
)}
</span>
</Modal.Header>
@@ -118,11 +133,15 @@ function CustomerGroupModal({
<Modal.Content>
<div className="gap-y-xlarge flex flex-col">
<div>
<h2 className="inter-base-semibold mb-base">Details</h2>
<h2 className="inter-base-semibold mb-base">
{t("groups-details", "Details")}
</h2>
<CustomerGroupGeneralForm form={nestedForm(form, "general")} />
</div>
<div>
<h2 className="inter-base-semibold mb-base">Metadata</h2>
<h2 className="inter-base-semibold mb-base">
{t("groups-metadata", "Metadata")}
</h2>
<MetadataForm form={nestedForm(form, "metadata")} />
</div>
</div>
@@ -137,10 +156,14 @@ function CustomerGroupModal({
type="button"
onClick={onClose}
>
Cancel
{t("groups-cancel", "Cancel")}
</Button>
<Button size="small" variant="primary" type="submit">
<span>{customerGroup ? "Edit" : "Publish"} Group</span>
<span>
{customerGroup
? t("groups-edit-group", "Edit Group")
: t("groups-publish-group", "Publish Group")}
</span>
</Button>
</div>
</Modal.Footer>
@@ -8,6 +8,7 @@ import {
useAdminRemoveCustomersFromCustomerGroup,
} from "medusa-react"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate, useParams } from "react-router-dom"
import BackButton from "../../../components/atoms/back-button"
@@ -40,10 +41,15 @@ const defaultQueryProps = {
* Placeholder for the customer groups list.
*/
function CustomersListPlaceholder() {
const { t } = useTranslation()
return (
<div className="center flex h-full min-h-[756px] items-center justify-center">
<span className="text-xs text-gray-400">
No customers in this group yet
{t(
"groups-no-customers-in-this-group-yet",
"No customers in this group yet"
)}
</span>
</div>
)
@@ -129,7 +135,7 @@ function CustomerGroupCustomersList(props: CustomerGroupCustomersListProps) {
return (
<BodyCard
title="Customers"
title={t("groups-customers", "Customers")}
actionables={actions}
className="min-h-[756px] w-full"
>
@@ -170,6 +176,7 @@ type CustomerGroupDetailsHeaderProps = {
function CustomerGroupDetailsHeader(props: CustomerGroupDetailsHeaderProps) {
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false)
const { t } = useTranslation()
const navigate = useNavigate()
const { mutate: deleteGroup } = useAdminDeleteCustomerGroup(
@@ -180,12 +187,12 @@ function CustomerGroupDetailsHeader(props: CustomerGroupDetailsHeaderProps) {
const actions: ActionType[] = [
{
label: "Edit",
label: t("groups-edit", "Edit"),
onClick: open,
icon: <EditIcon size={20} />,
},
{
label: "Delete",
label: t("groups-delete", "Delete"),
onClick: () => {
setShowDeleteConfirmation(true)
},
@@ -213,10 +220,13 @@ function CustomerGroupDetailsHeader(props: CustomerGroupDetailsHeaderProps) {
<DeletePrompt
onDelete={onDeleteConfirmed}
handleClose={handleConfirmDialogClose}
confirmText="Yes, delete"
heading="Delete the group"
successText="Group deleted"
text="Are you sure you want to delete this customer group?"
confirmText={t("groups-yes-delete", "Yes, delete")}
heading={t("groups-delete-the-group", "Delete the group")}
successText={t("groups-group-deleted", "Group deleted")}
text={t(
"groups-confirm-delete-customer-group",
"Are you sure you want to delete this customer group?"
)}
/>
)}
<CustomerGroupModal
@@ -265,7 +275,7 @@ function CustomerGroupDetails() {
<div className="-mt-4 pb-4">
<BackButton
path="/a/customers/groups"
label="Back to customer groups"
label={t("groups-back-to-customer-groups", "Back to customer groups")}
className="mb-4"
/>
<div className="gap-y-xsmall flex flex-col">
@@ -10,6 +10,7 @@ import { useWidgets } from "../../../providers/widget-provider"
import CustomersPageTableHeader from "../header"
import CustomerGroupModal from "./customer-group-modal"
import Details from "./details"
import { useTranslation } from "react-i18next"
/*
* Customer groups index page
@@ -17,10 +18,11 @@ import Details from "./details"
function Index() {
const { state, open, close } = useToggleState()
const { getWidgets } = useWidgets()
const { t } = useTranslation()
const actions = [
{
label: "New group",
label: t("groups-new-group", "New group"),
onClick: open,
icon: (
<span className="text-grey-90">
@@ -7,6 +7,8 @@ import {
useEffect,
useState,
} from "react"
import { useTranslation } from "react-i18next"
import useNotification from "../../../../../hooks/use-notification"
import { getErrorMessage } from "../../../../../utils/error-messages"
import {
@@ -68,6 +70,7 @@ export const ConditionsProvider = ({
discount,
children,
}: ConditionsProviderProps) => {
const { t } = useTranslation()
const [conditions, setConditions] = useState<ConditionMap>(defaultConditions)
const { mutate } = useAdminUpdateDiscount(discount.id)
@@ -130,8 +133,14 @@ export const ConditionsProvider = ({
{
onSuccess: () => {
notification(
"Conditions were successfully added",
"Discount conditions updated",
t(
"add-condition-conditions-were-successfully-added",
"Conditions were successfully added"
),
t(
"add-condition-discount-conditions-updated",
"Discount conditions updated"
),
"success"
)
},
@@ -175,9 +184,15 @@ export const ConditionsProvider = ({
}
export const useConditions = () => {
const { t } = useTranslation()
const context = useContext(ConditionsContext)
if (context === null) {
throw new Error("useConditions must be used within a ConditionsProvider")
throw new Error(
t(
"add-condition-use-conditions-must-be-used-within-a-conditions-provider",
"useConditions must be used within a ConditionsProvider"
)
)
}
return context
}
@@ -1,11 +1,12 @@
import { useAdminCollections } from "medusa-react"
import { useContext, useState } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../../../../../../components/fundamentals/button"
import Modal from "../../../../../../../components/molecules/modal"
import { LayeredModalContext } from "../../../../../../../components/molecules/modal/layered-modal"
import { SelectableTable } from "../../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../../hooks/use-query-filters"
import { defaultQueryProps } from "../../../../..//new/discount-form/condition-tables/shared/common"
import { defaultQueryProps } from "../../../../../new/discount-form/condition-tables/shared/common"
import {
CollectionRow,
CollectionsHeader,
@@ -14,6 +15,7 @@ import {
import { useEditConditionContext } from "../../edit-condition-provider"
const AddCollectionConditionsScreen = () => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { pop } = useContext(LayeredModalContext)
@@ -38,7 +40,7 @@ const AddCollectionConditionsScreen = () => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search...",
searchPlaceholder: t("collections-search", "Search..."),
}}
resourceName="Collections"
totalCount={count ?? 0}
@@ -55,21 +57,21 @@ const AddCollectionConditionsScreen = () => {
<Modal.Footer>
<div className="space-x-xsmall flex w-full justify-end">
<Button variant="secondary" size="small" onClick={pop}>
Cancel
{t("collections-cancel", "Cancel")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => saveAndGoBack(selectedResources, () => refetch())}
>
Save and go back
{t("collections-save-and-go-back", "Save and go back")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => saveAndClose(selectedResources)}
>
Save and close
{t("collections-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>
@@ -1,5 +1,7 @@
import { useAdminCustomerGroups } from "medusa-react"
import { useContext, useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../../hooks/use-query-filters"
@@ -14,6 +16,7 @@ import Button from "../../../../../../../components/fundamentals/button"
import { LayeredModalContext } from "../../../../../../../components/molecules/modal/layered-modal"
const AddCustomerGroupsConditionsScreen = () => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { pop } = useContext(LayeredModalContext)
@@ -38,7 +41,7 @@ const AddCustomerGroupsConditionsScreen = () => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search...",
searchPlaceholder: t("customer-groups-search", "Search..."),
}}
resourceName="Groups"
totalCount={count ?? 0}
@@ -55,21 +58,21 @@ const AddCustomerGroupsConditionsScreen = () => {
<Modal.Footer>
<div className="space-x-xsmall flex w-full justify-end">
<Button variant="secondary" size="small" onClick={pop}>
Cancel
{t("customer-groups-cancel", "Cancel")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => saveAndGoBack(selectedResources, () => refetch())}
>
Save and go back
{t("customer-groups-save-and-go-back", "Save and go back")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => saveAndClose(selectedResources)}
>
Save and close
{t("customer-groups-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>
@@ -1,5 +1,6 @@
import { useAdminProductTypes } from "medusa-react"
import { useContext, useState } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../../../../../../components/fundamentals/button"
import Modal from "../../../../../../../components/molecules/modal"
import { LayeredModalContext } from "../../../../../../../components/molecules/modal/layered-modal"
@@ -14,6 +15,7 @@ import {
import { useEditConditionContext } from "../../edit-condition-provider"
const AddTypesConditionsScreen = () => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { pop } = useContext(LayeredModalContext)
@@ -40,7 +42,7 @@ const AddTypesConditionsScreen = () => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search...",
searchPlaceholder: t("product-types-search", "Search..."),
}}
resourceName="Types"
totalCount={count ?? 0}
@@ -57,21 +59,21 @@ const AddTypesConditionsScreen = () => {
<Modal.Footer>
<div className="space-x-xsmall flex w-full justify-end">
<Button variant="secondary" size="small" onClick={pop}>
Cancel
{t("product-types-cancel", "Cancel")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => saveAndGoBack(selectedResources, () => refetch())}
>
Save and go back
{t("product-types-save-and-go-back", "Save and go back")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => saveAndClose(selectedResources)}
>
Save and close
{t("product-types-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>
@@ -1,11 +1,12 @@
import { useAdminProducts } from "medusa-react"
import { useContext, useState } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../../../../../../components/fundamentals/button"
import Modal from "../../../../../../../components/molecules/modal"
import { LayeredModalContext } from "../../../../../../../components/molecules/modal/layered-modal"
import { SelectableTable } from "../../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../../hooks/use-query-filters"
import { defaultQueryProps } from "../../../../..//new/discount-form/condition-tables/shared/common"
import { defaultQueryProps } from "../../../../../new/discount-form/condition-tables/shared/common"
import {
ProductRow,
ProductsHeader,
@@ -14,6 +15,7 @@ import {
import { useEditConditionContext } from "../../edit-condition-provider"
const AddProductConditionsScreen = () => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { pop } = useContext(LayeredModalContext)
@@ -35,7 +37,7 @@ const AddProductConditionsScreen = () => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search...",
searchPlaceholder: t("products-search", "Search..."),
}}
resourceName="Products"
totalCount={count ?? 0}
@@ -52,21 +54,21 @@ const AddProductConditionsScreen = () => {
<Modal.Footer>
<div className="space-x-xsmall flex w-full justify-end">
<Button variant="secondary" size="small" onClick={pop}>
Cancel
{t("products-cancel", "Cancel")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => saveAndGoBack(selectedResources)}
>
Save and go back
{t("products-save-and-go-back", "Save and go back")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => saveAndClose(selectedResources)}
>
Save and close
{t("products-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>
@@ -1,5 +1,6 @@
import { useAdminProductTags } from "medusa-react"
import { useContext, useState } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../../../../../../components/fundamentals/button"
import Modal from "../../../../../../../components/molecules/modal"
import { LayeredModalContext } from "../../../../../../../components/molecules/modal/layered-modal"
@@ -14,6 +15,7 @@ import {
import { useEditConditionContext } from "../../edit-condition-provider"
const AddTagsConditionsScreen = () => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { pop } = useContext(LayeredModalContext)
@@ -36,7 +38,7 @@ const AddTagsConditionsScreen = () => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search...",
searchPlaceholder: t("tags-search", "Search..."),
}}
resourceName="Tags"
totalCount={count ?? 0}
@@ -53,21 +55,21 @@ const AddTagsConditionsScreen = () => {
<Modal.Footer>
<div className="space-x-xsmall flex w-full justify-end">
<Button variant="secondary" size="small" onClick={pop}>
Cancel
{t("tags-cancel", "Cancel")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => saveAndGoBack(selectedResources)}
>
Save and go back
{t("tags-save-and-go-back", "Save and go back")}
</Button>
<Button
variant="primary"
size="small"
onClick={() => saveAndClose(selectedResources)}
>
Save and close
{t("tags-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>
@@ -6,8 +6,10 @@ import AddCustomerGroupsConditionsScreen from "./add-condition-resources/custome
import AddTypesConditionsScreen from "./add-condition-resources/product-types/add-types"
import AddProductConditionsScreen from "./add-condition-resources/products/add-products"
import AddTagsConditionsScreen from "./add-condition-resources/tags/add-tags"
import { useTranslation } from "react-i18next"
export const useAddConditionsModalScreen = (condition: DiscountCondition) => {
const { t } = useTranslation()
const { pop } = React.useContext(LayeredModalContext)
const renderModalScreen = () => {
@@ -26,7 +28,7 @@ export const useAddConditionsModalScreen = (condition: DiscountCondition) => {
}
return {
title: `Add conditions`,
title: t("edit-condition-add-conditions", "Add conditions"),
onBack: pop,
view: renderModalScreen(),
}
@@ -1,5 +1,6 @@
import clsx from "clsx"
import { useContext } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../../../../components/fundamentals/button"
import PlusIcon from "../../../../../components/fundamentals/icons/plus-icon"
import { LayeredModalContext } from "../../../../../components/molecules/modal/layered-modal"
@@ -17,6 +18,7 @@ const ExistingConditionTableActions = ({
onDeselect,
onRemove,
}: Props) => {
const { t } = useTranslation()
const { condition } = useEditConditionContext()
const addConditionsModalScreen = useAddConditionsModalScreen(condition)
@@ -35,7 +37,9 @@ const ExistingConditionTableActions = ({
<div className={clsx("transition-all duration-200", classes)}>
<div className="mb-2 flex h-[34px] items-center divide-x">
<span className="inter-small-regular text-grey-50 mr-3">
{numberOfSelectedRows} selected
{t("edit-condition-selected-with-count", "{{count}}", {
count: numberOfSelectedRows,
})}
</span>
<div className="space-x-xsmall flex pl-3">
<Button
@@ -44,7 +48,7 @@ const ExistingConditionTableActions = ({
variant="ghost"
className="border-grey-20 border"
>
Deselect
{t("edit-condition-deselect", "Deselect")}
</Button>
<Button
onClick={onRemove}
@@ -52,7 +56,7 @@ const ExistingConditionTableActions = ({
variant="ghost"
className="border-grey-20 border text-rose-50"
>
Remove
{t("edit-condition-remove", "Remove")}
</Button>
</div>
</div>
@@ -63,7 +67,7 @@ const ExistingConditionTableActions = ({
className="border-grey-20 border"
onClick={() => push(addConditionsModalScreen)}
>
<PlusIcon size={20} /> Add
<PlusIcon size={20} /> {t("edit-condition-add", "Add")}
</Button>
</div>
</div>
@@ -13,6 +13,7 @@ import CustomerGroupsConditionsTable from "./add-condition-resources/customer-gr
import { Discount, DiscountCondition } from "@medusajs/medusa"
import { capitalize } from "lodash"
import { getTitle } from "../../../utils"
import { useTranslation } from "react-i18next"
type Props = {
open: boolean
@@ -22,6 +23,7 @@ type Props = {
}
const EditConditionsModal = ({ open, condition, discount, onClose }: Props) => {
const { t } = useTranslation()
const context = useContext(LayeredModalContext)
const renderModalContext = () => {
@@ -49,7 +51,13 @@ const EditConditionsModal = ({ open, condition, discount, onClose }: Props) => {
<Modal.Body>
<Modal.Header handleClose={onClose}>
<h1 className="inter-xlarge-semibold">
Edit {capitalize(getTitle(condition?.type))} in Discount Condition
{t(
"edit-condition-title",
"Edit {{type}} in Discount Condition",
{
type: capitalize(getTitle(condition?.type, t)),
}
)}
</h1>
</Modal.Header>
{renderModalContext()}
@@ -61,7 +69,7 @@ const EditConditionsModal = ({ open, condition, discount, onClose }: Props) => {
type="button"
onClick={onClose}
>
Close
{t("edit-condition-close", "Close")}
</Button>
</div>
</Modal.Footer>
@@ -4,6 +4,7 @@ import {
useAdminDeleteDiscountConditionResourceBatch,
} from "medusa-react"
import { createContext, ReactNode, useContext } from "react"
import { useTranslation } from "react-i18next"
import { LayeredModalContext } from "../../../../../components/molecules/modal/layered-modal"
import useNotification from "../../../../../hooks/use-notification"
@@ -33,6 +34,7 @@ export const EditConditionProvider = ({
onClose,
children,
}: ConditionsProviderProps) => {
const { t } = useTranslation()
const notification = useNotification()
const { pop, reset } = useContext(LayeredModalContext)
@@ -54,14 +56,24 @@ export const EditConditionProvider = ({
{
onSuccess: () => {
notification(
"Success",
"The resources were successfully added",
t("edit-condition-success", "Success"),
t(
"edit-condition-the-resources-were-successfully-added",
"The resources were successfully added"
),
"success"
)
onSuccessCallback?.()
},
onError: () =>
notification("Error", "Failed to add resources", "error"),
notification(
t("edit-condition-error", "Error"),
t(
"edit-condition-failed-to-add-resources",
"Failed to add resources"
),
"error"
),
}
)
}
@@ -72,13 +84,23 @@ export const EditConditionProvider = ({
{
onSuccess: () => {
notification(
"Success",
"The resources were successfully removed",
t("edit-condition-success", "Success"),
t(
"edit-condition-the-resources-were-successfully-removed",
"The resources were successfully removed"
),
"success"
)
},
onError: () =>
notification("Error", "Failed to remove resources", "error"),
notification(
t("edit-condition-error", "Error"),
t(
"edit-condition-failed-to-remove-resources",
"Failed to remove resources"
),
"error"
),
}
)
}
@@ -115,7 +137,10 @@ export const useEditConditionContext = () => {
const context = useContext(EditConditionContext)
if (context === null) {
throw new Error(
"useEditConditionContext must be used within an EditConditionProvider"
t(
"edit-condition-use-edit-condition-context-must-be-used-within-an-edit-condition-provider",
"useEditConditionContext must be used within an EditConditionProvider"
)
)
}
return context
@@ -7,6 +7,7 @@ import AddCondition from "./add-condition"
import { ConditionsProvider } from "./add-condition/conditions-provider"
import EditConditionsModal from "./edit-condition/edit-condition-modal"
import { useDiscountConditions } from "./use-discount-conditions"
import { useTranslation } from "react-i18next"
type DiscountDetailsConditionsProps = {
discount: Discount
@@ -15,6 +16,7 @@ type DiscountDetailsConditionsProps = {
const DiscountDetailsConditions: React.FC<DiscountDetailsConditionsProps> = ({
discount,
}) => {
const { t } = useTranslation()
const [show, setShow] = useState(false)
const { conditions, selectedCondition, deSelectCondition } =
@@ -24,12 +26,12 @@ const DiscountDetailsConditions: React.FC<DiscountDetailsConditionsProps> = ({
<ConditionsProvider discount={discount}>
<div>
<BodyCard
title="Conditions"
title={t("conditions-conditions", "Conditions")}
className="min-h-[200px]"
forceDropdown
actionables={[
{
label: "Add condition",
label: t("conditions-add-condition-label", "Add condition"),
icon: <PlusIcon size={16} />,
onClick: () => setShow(true),
},
@@ -57,7 +59,10 @@ const DiscountDetailsConditions: React.FC<DiscountDetailsConditionsProps> = ({
) : (
<div className="gap-y-small flex flex-1 flex-col items-center justify-center">
<span className="inter-base-regular text-grey-50">
This discount has no conditions
{t(
"conditions-this-discount-has-no-conditions",
"This discount has no conditions"
)}
</span>
</div>
)}
@@ -5,14 +5,17 @@ import {
useAdminGetDiscountCondition,
} from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import EditIcon from "../../../../components/fundamentals/icons/edit-icon"
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
import { ActionType } from "../../../../components/molecules/actionables"
import useNotification from "../../../../hooks/use-notification"
import { getErrorMessage } from "../../../../utils/error-messages"
import { DiscountConditionType } from "../../types"
import { TFunction } from "i18next"
export const useDiscountConditions = (discount: Discount) => {
const { t } = useTranslation()
const [selectedCondition, setSelectedCondition] = useState<string | null>(
null
)
@@ -34,11 +37,19 @@ export const useDiscountConditions = (discount: Discount) => {
const removeCondition = (conditionId: string) => {
mutate(conditionId, {
onSuccess: () => {
notification("Success", "Condition removed", "success")
notification(
t("conditions-success", "Success"),
t("conditions-condition-removed", "Condition removed"),
"success"
)
refetch()
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("conditions-error", "Error"),
getErrorMessage(error),
"error"
)
},
})
}
@@ -46,16 +57,16 @@ export const useDiscountConditions = (discount: Discount) => {
const itemized = discount.rule.conditions.map((condition) => ({
type: condition.type,
title: getTitle(condition.type),
description: getDescription(condition.type),
description: getDescription(condition.type, t),
actions: [
{
label: "Edit condition",
label: t("conditions-edit-condition", "Edit condition"),
icon: <EditIcon size={16} />,
variant: "ghost",
onClick: () => setSelectedCondition(condition.id),
},
{
label: "Delete condition",
label: t("conditions-delete-condition", "Delete condition"),
icon: <TrashIcon size={16} />,
variant: "danger",
onClick: () => removeCondition(condition.id),
@@ -89,17 +100,32 @@ const getTitle = (type: DiscountConditionType) => {
}
}
const getDescription = (type: DiscountConditionType) => {
const getDescription = (type: DiscountConditionType, t: TFunction) => {
switch (type) {
case DiscountConditionType.PRODUCTS:
return "Discount is applicable to specific products"
return t(
"conditions-discount-is-applicable-to-specific-products",
"Discount is applicable to specific products"
)
case DiscountConditionType.PRODUCT_COLLECTIONS:
return "Discount is applicable to specific collections"
return t(
"conditions-discount-is-applicable-to-specific-collections",
"Discount is applicable to specific collections"
)
case DiscountConditionType.PRODUCT_TAGS:
return "Discount is applicable to specific product tags"
return t(
"conditions-discount-is-applicable-to-specific-product-tags",
"Discount is applicable to specific product tags"
)
case DiscountConditionType.PRODUCT_TYPES:
return "Discount is applicable to specific product types"
return t(
"conditions-discount-is-applicable-to-specific-product-types",
"Discount is applicable to specific product types"
)
case DiscountConditionType.CUSTOMER_GROUPS:
return "Discount is applicable to specific customer groups"
return t(
"conditions-discount-is-applicable-to-specific-customer-groups",
"Discount is applicable to specific customer groups"
)
}
}
@@ -2,6 +2,7 @@ import { Discount } from "@medusajs/medusa"
import { useAdminUpdateDiscount } from "medusa-react"
import React, { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import DiscountConfigurationForm, {
DiscountConfigurationFormType,
} from "../../../../components/forms/discount/discount-configuration-form"
@@ -26,6 +27,8 @@ const EditConfigurations: React.FC<EditConfigurationsProps> = ({
onClose,
open,
}) => {
const { t } = useTranslation()
const { mutate, isLoading } = useAdminUpdateDiscount(discount.id)
const notification = useNotification()
@@ -48,12 +51,23 @@ const EditConfigurations: React.FC<EditConfigurationsProps> = ({
},
{
onSuccess: ({ discount }) => {
notification("Success", "Discount updated successfully", "success")
notification(
t("configurations-success", "Success"),
t(
"configurations-discount-updated-successfully",
"Discount updated successfully"
),
"success"
)
reset(getDefaultValues(discount))
onClose()
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("configurations-error", "Error"),
getErrorMessage(error),
"error"
)
},
}
)
@@ -69,7 +83,9 @@ const EditConfigurations: React.FC<EditConfigurationsProps> = ({
<Modal open={open} handleClose={onClose} isLargeModal>
<Modal.Body>
<Modal.Header handleClose={onClose}>
<h1 className="inter-xlarge-semibold">Edit configurations</h1>
<h1 className="inter-xlarge-semibold">
{t("configurations-edit-configurations", "Edit configurations")}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
@@ -84,7 +100,7 @@ const EditConfigurations: React.FC<EditConfigurationsProps> = ({
type="button"
onClick={onClose}
>
Cancel
{t("configurations-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -94,7 +110,7 @@ const EditConfigurations: React.FC<EditConfigurationsProps> = ({
loading={isLoading}
disabled={isLoading}
>
Save
{t("configurations-save", "Save")}
</Button>
</div>
</Modal.Footer>
@@ -1,5 +1,6 @@
import { Discount } from "@medusajs/medusa"
import React from "react"
import { useTranslation } from "react-i18next"
import EditIcon from "../../../../components/fundamentals/icons/edit-icon"
import NumberedItem from "../../../../components/molecules/numbered-item"
import BodyCard from "../../../../components/organisms/body-card"
@@ -12,17 +13,21 @@ type ConfigurationsProps = {
}
const Configurations: React.FC<ConfigurationsProps> = ({ discount }) => {
const { t } = useTranslation()
const configurations = useDiscountConfigurations(discount)
const { state, open, close } = useToggleState()
return (
<>
<BodyCard
title={"Configurations"}
title={t("configurations-configurations", "Configurations")}
className="min-h-[200px]"
actionables={[
{
label: "Edit configurations",
label: t(
"configurations-edit-configurations",
"Edit configurations"
),
onClick: open,
icon: <EditIcon size={20} />,
},
@@ -1,4 +1,5 @@
import { ReactNode } from "react"
import { useTranslation } from "react-i18next"
import { ActionType } from "../../../../components/molecules/actionables"
import ClockIcon from "../../../../components/fundamentals/icons/clock-icon"
@@ -32,23 +33,27 @@ const CommonDescription = ({ text }) => (
)
const useDiscountConfigurations = (discount: Discount) => {
const { t } = useTranslation()
const updateDiscount = useAdminUpdateDiscount(discount.id)
const notification = useNotification()
const conditions: displaySetting[] = []
conditions.push({
title: "Start date",
title: t("configurations-start-date", "Start date"),
description: <DisplaySettingsDateDescription date={discount.starts_at} />,
})
if (discount.ends_at) {
conditions.push({
title: "End date",
title: t("configurations-end-date", "End date"),
description: <DisplaySettingsDateDescription date={discount.ends_at} />,
actions: [
{
label: "Delete configuration",
label: t(
"configurations-delete-configuration",
"Delete configuration"
),
icon: <TrashIcon size={20} />,
variant: "danger",
onClick: async () =>
@@ -57,13 +62,20 @@ const useDiscountConfigurations = (discount: Discount) => {
{
onSuccess: () => {
notification(
"Success",
"Discount end date removed",
t("configurations-success", "Success"),
t(
"configurations-discount-end-date-removed",
"Discount end date removed"
),
"success"
)
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("configurations-error", "Error"),
getErrorMessage(error),
"error"
)
},
}
),
@@ -73,13 +85,16 @@ const useDiscountConfigurations = (discount: Discount) => {
}
if (discount.usage_limit) {
conditions.push({
title: "Number of redemptions",
title: t("configurations-number-of-redemptions", "Number of redemptions"),
description: (
<CommonDescription text={discount.usage_limit.toLocaleString("en")} />
),
actions: [
{
label: "Delete configuration",
label: t(
"configurations-delete-configuration",
"Delete configuration"
),
icon: <TrashIcon size={20} />,
variant: "danger",
onClick: async () =>
@@ -87,10 +102,21 @@ const useDiscountConfigurations = (discount: Discount) => {
{ usage_limit: null },
{
onSuccess: () => {
notification("Success", "Redemption limit removed", "success")
notification(
t("configurations-success", "Success"),
t(
"configurations-redemption-limit-removed",
"Redemption limit removed"
),
"success"
)
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("configurations-error", "Error"),
getErrorMessage(error),
"error"
)
},
}
),
@@ -110,7 +136,7 @@ const useDiscountConfigurations = (discount: Discount) => {
),
actions: [
{
label: "Delete setting",
label: t("configurations-delete-setting", "Delete setting"),
icon: <TrashIcon size={20} />,
variant: "danger",
onClick: async () =>
@@ -119,13 +145,20 @@ const useDiscountConfigurations = (discount: Discount) => {
{
onSuccess: () => {
notification(
"Success",
"Discount duration removed",
t("configurations-success", "Success"),
t(
"configurations-discount-duration-removed",
"Discount duration removed"
),
"success"
)
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("configurations-error", "Error"),
getErrorMessage(error),
"error"
)
},
}
),
@@ -2,6 +2,7 @@ import { Discount } from "@medusajs/medusa"
import { useAdminUpdateDiscount } from "medusa-react"
import React, { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import DiscountGeneralForm, {
DiscountGeneralFormType,
} from "../../../../components/forms/discount/discount-general-form"
@@ -32,6 +33,7 @@ const EditGeneral: React.FC<EditGeneralProps> = ({
open,
onClose,
}) => {
const { t } = useTranslation()
const { mutate, isLoading } = useAdminUpdateDiscount(discount.id)
const notification = useNotification()
@@ -56,11 +58,22 @@ const EditGeneral: React.FC<EditGeneralProps> = ({
},
{
onSuccess: () => {
notification("Success", "Discount updated successfully", "success")
notification(
t("general-success", "Success"),
t(
"general-discount-updated-successfully",
"Discount updated successfully"
),
"success"
)
onClose()
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("general-error", "Error"),
getErrorMessage(error),
"error"
)
},
}
)
@@ -76,13 +89,17 @@ const EditGeneral: React.FC<EditGeneralProps> = ({
<Modal open={open} handleClose={onClose}>
<Modal.Body>
<Modal.Header handleClose={onClose}>
<h1 className="inter-xlarge-semibold">Edit general information</h1>
<h1 className="inter-xlarge-semibold">
{t("general-edit-general-information", "Edit general information")}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
<div className="gap-y-xlarge flex flex-col">
<div>
<h2 className="inter-base-semibold mb-base">Details</h2>
<h2 className="inter-base-semibold mb-base">
{t("general-details", "Details")}
</h2>
<DiscountGeneralForm
form={nestedForm(form, "general")}
type={discount.rule.type}
@@ -90,7 +107,9 @@ const EditGeneral: React.FC<EditGeneralProps> = ({
/>
</div>
<div>
<h2 className="inter-base-semibold mb-base">Metadata</h2>
<h2 className="inter-base-semibold mb-base">
{t("general-metadata", "Metadata")}
</h2>
<MetadataForm form={nestedForm(form, "metadata")} />
</div>
</div>
@@ -103,7 +122,7 @@ const EditGeneral: React.FC<EditGeneralProps> = ({
type="button"
onClick={onClose}
>
Cancel
{t("general-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -112,7 +131,7 @@ const EditGeneral: React.FC<EditGeneralProps> = ({
disabled={isLoading}
loading={isLoading}
>
Save and close
{t("general-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>
@@ -2,6 +2,7 @@ import { Discount } from "@medusajs/medusa"
import { useAdminDeleteDiscount, useAdminUpdateDiscount } from "medusa-react"
import React from "react"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import Badge from "../../../../components/fundamentals/badge"
import EditIcon from "../../../../components/fundamentals/icons/edit-icon"
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
@@ -14,12 +15,14 @@ import useToggleState from "../../../../hooks/use-toggle-state"
import { getErrorMessage } from "../../../../utils/error-messages"
import { formatAmountWithSymbol } from "../../../../utils/prices"
import EditGeneral from "./edit-general"
import { TFunction } from "i18next"
type GeneralProps = {
discount: Discount
}
const General: React.FC<GeneralProps> = ({ discount }) => {
const { t } = useTranslation()
const dialog = useImperativeDialog()
const navigate = useNavigate()
const notification = useNotification()
@@ -28,17 +31,31 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
const onDelete = async () => {
const shouldDelete = await dialog({
heading: "Delete Promotion",
text: "Are you sure you want to delete this promotion?",
heading: t("general-delete-promotion", "Delete Promotion"),
text: t(
"general-confirm-delete-promotion",
"Are you sure you want to delete this promotion?"
),
})
if (shouldDelete) {
deletediscount.mutate(undefined, {
onSuccess: () => {
notification("Success", "Promotion deleted successfully", "success")
notification(
t("general-success", "Success"),
t(
"general-promotion-deleted-successfully",
"Promotion deleted successfully"
),
"success"
)
navigate("/a/discounts/")
},
onError: (err) => {
notification("Error", getErrorMessage(err), "error")
notification(
t("general-error", "Error"),
getErrorMessage(err),
"error"
)
},
})
}
@@ -51,15 +68,26 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
},
{
onSuccess: ({ discount: { is_disabled } }) => {
const pastTense = !is_disabled ? "published" : "drafted"
notification(
"Success",
`Discount ${pastTense} successfully`,
t("general-success", "Success"),
!is_disabled
? t(
"general-discount-published-successfully",
"Discount published successfully"
)
: t(
"general-discount-drafted-successfully",
"Discount drafted successfully"
),
"success"
)
},
onError: (err) => {
notification("Error", getErrorMessage(err), "error")
notification(
t("general-error", "Error"),
getErrorMessage(err),
"error"
)
},
}
)
@@ -69,12 +97,12 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
const actionables: ActionType[] = [
{
label: "Edit general information",
label: t("general-edit-general-information", "Edit general information"),
onClick: open,
icon: <EditIcon size={20} />,
},
{
label: "Delete discount",
label: t("general-delete-discount", "Delete discount"),
onClick: onDelete,
variant: "danger",
icon: <TrashIcon size={20} />,
@@ -95,15 +123,15 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
<span>
<Badge variant="default">
<span className="text-grey-90 inter-small-regular">
{"Template discount"}
{t("general-template-discount", "Template discount")}
</span>
</Badge>
</span>
)}
<StatusSelector
isDraft={discount?.is_disabled}
activeState="Published"
draftState="Draft"
activeState={t("general-published", "Published")}
draftState={t("general-draft", "Draft")}
onChange={onStatusChange}
/>
</div>
@@ -111,9 +139,9 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
>
<div className="flex">
<div className="border-grey-20 border-l pl-6">
{getPromotionDescription(discount)}
{getPromotionDescription(discount, t)}
<span className="inter-small-regular text-grey-50">
Discount Amount
{t("general-discount-amount", "Discount Amount")}
</span>
</div>
<div className="border-grey-20 ml-12 border-l pl-6">
@@ -121,7 +149,7 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
{discount.regions.length.toLocaleString("en-US")}
</h2>
<span className="inter-small-regular text-grey-50">
Valid Regions
{t("general-valid-regions", "Valid Regions")}
</span>
</div>
<div className="border-grey-20 ml-12 border-l pl-6">
@@ -129,7 +157,7 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
{discount.usage_count.toLocaleString("en-US")}
</h2>
<span className="inter-small-regular text-grey-50">
Total Redemptions
{t("general-total-redemptions", "Total Redemptions")}
</span>
</div>
</div>
@@ -140,7 +168,7 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
)
}
const getPromotionDescription = (discount: Discount) => {
const getPromotionDescription = (discount: Discount, t: TFunction) => {
switch (discount.rule.type) {
case "fixed":
return (
@@ -167,10 +195,12 @@ const getPromotionDescription = (discount: Discount) => {
)
case "free_shipping":
return (
<h2 className="inter-xlarge-regular text-grey-90">{`FREE SHIPPING`}</h2>
<h2 className="inter-xlarge-regular text-grey-90">
{t("general-free-shipping", "FREE SHIPPING")}
</h2>
)
default:
return "Unknown discount type"
return t("general-unknown-discount-type", "Unknown discount type")
}
}
@@ -1,6 +1,7 @@
import { useAdminDeleteDiscount, useAdminDiscount } from "medusa-react"
import { useState } from "react"
import { useNavigate, useParams } from "react-router-dom"
import { useTranslation } from "react-i18next"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import WidgetContainer from "../../../components/extensions/widget-container"
@@ -16,6 +17,7 @@ import Configurations from "./configurations"
import General from "./general"
const Edit = () => {
const { t } = useTranslation()
const { id } = useParams()
const navigate = useNavigate()
@@ -35,10 +37,18 @@ const Edit = () => {
const handleDelete = () => {
deleteDiscount.mutate(undefined, {
onSuccess: () => {
notification("Success", "Discount deleted", "success")
notification(
t("details-success", "Success"),
t("details-discount-deleted", "Discount deleted"),
"success"
)
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("details-error", "Error"),
getErrorMessage(error),
"error"
)
},
})
}
@@ -72,15 +82,18 @@ const Edit = () => {
<DeletePrompt
handleClose={() => setShowDelete(!showDelete)}
onDelete={async () => handleDelete()}
successText="Discount deleted"
confirmText="Yes, delete"
text="Are you sure you want to delete this discount?"
heading="Delete discount"
successText={t("details-discount-deleted", "Discount deleted")}
confirmText={t("details-yes-delete", "Yes, delete")}
text={t(
"details-confirm-delete-discount",
"Are you sure you want to delete this discount?"
)}
heading={t("details-delete-discount", "Delete discount")}
/>
)}
<BackButton
label="Back to Discounts"
label={t("details-back-to-discounts", "Back to Discounts")}
path="/a/discounts"
className="mb-xsmall"
/>
@@ -109,7 +122,10 @@ const Edit = () => {
/>
)
})}
<RawJSON data={discount} title="Raw discount" />
<RawJSON
data={discount}
title={t("details-raw-discount", "Raw discount")}
/>
</DiscountFormProvider>
</div>
</div>
@@ -1,5 +1,6 @@
import { useState } from "react"
import { Route, Routes } from "react-router-dom"
import { useTranslation } from "react-i18next"
import Fade from "../../components/atoms/fade-wrapper"
import Spacer from "../../components/atoms/spacer"
import RouteContainer from "../../components/extensions/route-container"
@@ -16,11 +17,12 @@ import DiscountForm from "./new/discount-form"
import { DiscountFormProvider } from "./new/discount-form/form/discount-form-context"
const DiscountIndex = () => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const actionables = [
{
label: "Add Discount",
label: t("discounts-add-discount", "Add Discount"),
onClick: () => setIsOpen(true),
icon: <PlusIcon size={20} />,
},
@@ -10,6 +10,7 @@ import { AddConditionSelectorProps, ConditionMap } from "../../types"
import useConditionModalItems, {
ConditionItem,
} from "./use-condition-modal-items"
import { useTranslation } from "react-i18next"
type AddConditionsModalProps = AddConditionSelectorProps & {
isDetails?: boolean
@@ -23,6 +24,7 @@ const AddConditionsModal = ({
save,
isDetails = false,
}: AddConditionsModalProps) => {
const { t } = useTranslation()
const layeredModalContext = useContext(LayeredModalContext)
const [items, setItems] = useState<ConditionItem[]>(
@@ -49,10 +51,20 @@ const AddConditionsModal = ({
<LayeredModal context={layeredModalContext} handleClose={onClose}>
<Modal.Body className="flex h-[calc(100vh-134px)] flex-col">
<Modal.Header handleClose={onClose}>
<span className="inter-xlarge-semibold">Add Conditions</span>
<span className="inter-xlarge-semibold">
{t("discount-form-add-conditions", "Add Conditions")}
</span>
<span className="text-grey-90 mt-6 flex items-center gap-1 font-semibold">
Choose a condition type{" "}
<IconTooltip content="You can only add one of each type of condition" />
{t(
"discount-form-choose-a-condition-type",
"Choose a condition type"
)}{" "}
<IconTooltip
content={t(
"discount-form-you-can-only-add-one-of-each-type-of-condition",
"You can only add one of each type of condition"
)}
/>
</span>
</Modal.Header>
@@ -62,7 +74,10 @@ const AddConditionsModal = ({
) : (
<div className="flex h-full flex-1 flex-col items-center justify-center">
<span className="inter-base-regular text-grey-40">
You cannot add any more conditions
{t(
"discount-form-you-cannot-add-any-more-conditions",
"You cannot add any more conditions"
)}
</span>
</div>
)}
@@ -76,7 +91,7 @@ const AddConditionsModal = ({
size="small"
onClick={onClose}
>
Cancel
{t("discount-form-cancel", "Cancel")}
</Button>
<Button
onClick={() => {
@@ -89,7 +104,7 @@ const AddConditionsModal = ({
className="text-small w-32 justify-center"
variant="primary"
>
Save
{t("discount-form-save", "Save")}
</Button>
</div>
</Modal.Footer>
@@ -3,6 +3,7 @@ import Button from "../../../../../../components/fundamentals/button"
import { LayeredModalContext } from "../../../../../../components/molecules/modal/layered-modal"
import { DiscountConditionOperator } from "../../../../types"
import { useDiscountForm } from "../../form/discount-form-context"
import { useTranslation } from "react-i18next"
type AddConditionFooterProps = {
type:
@@ -22,6 +23,7 @@ const AddConditionFooter: React.FC<AddConditionFooterProps> = ({
operator,
onClose,
}) => {
const { t } = useTranslation()
const { pop, reset } = useContext(LayeredModalContext)
const { updateCondition } = useDiscountForm()
@@ -35,7 +37,7 @@ const AddConditionFooter: React.FC<AddConditionFooterProps> = ({
reset()
}}
>
Cancel
{t("add-condition-tables-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -49,7 +51,7 @@ const AddConditionFooter: React.FC<AddConditionFooterProps> = ({
pop()
}}
>
Save and add more
{t("add-condition-tables-save-and-add-more", "Save and add more")}
</Button>
<Button
variant="primary"
@@ -60,7 +62,7 @@ const AddConditionFooter: React.FC<AddConditionFooterProps> = ({
reset()
}}
>
Save and close
{t("add-condition-tables-save-and-close", "Save and close")}
</Button>
</div>
)
@@ -1,5 +1,7 @@
import { useAdminCollections } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -20,6 +22,7 @@ import AddConditionFooter from "./add-condition-footer"
const AddCollectionConditionSelector = ({
onClose,
}: AddConditionSelectorProps) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useDiscountForm()
@@ -62,7 +65,10 @@ const AddCollectionConditionSelector = ({
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search by title...",
searchPlaceholder: t(
"add-condition-tables-search-by-title",
"Search by title..."
),
}}
resourceName="Collections"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminCustomerGroups } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -20,6 +21,7 @@ import AddConditionFooter from "./add-condition-footer"
const AddCustomerGroupConditionSelector = ({
onClose,
}: AddConditionSelectorProps) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useDiscountForm()
@@ -59,7 +61,10 @@ const AddCustomerGroupConditionSelector = ({
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search groups...",
searchPlaceholder: t(
"add-condition-tables-search-groups",
"Search groups..."
),
}}
resourceName="Customer groups"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminProducts } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -20,6 +21,7 @@ import AddConditionFooter from "./add-condition-footer"
const AddProductConditionSelector = ({
onClose,
}: AddConditionSelectorProps) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useDiscountForm()
@@ -55,7 +57,10 @@ const AddProductConditionSelector = ({
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search products...",
searchPlaceholder: t(
"add-condition-tables-search-products",
"Search products..."
),
}}
resourceName="Products"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminProductTags } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -14,6 +15,7 @@ import { TagColumns, TagHeader, TagRow } from "../shared/tags"
import AddConditionFooter from "./add-condition-footer"
const AddTagConditionSelector = ({ onClose }: AddConditionSelectorProps) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useDiscountForm()
@@ -45,7 +47,10 @@ const AddTagConditionSelector = ({ onClose }: AddConditionSelectorProps) => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search by tag...",
searchPlaceholder: t(
"add-condition-tables-search-by-tag",
"Search by tag..."
),
}}
resourceName="Tags"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminProductTypes } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -14,6 +15,7 @@ import { TypeRow, TypesHeader, useTypesColumns } from "../shared/types"
import AddConditionFooter from "./add-condition-footer"
const AddTypeConditionSelector = ({ onClose }: AddConditionSelectorProps) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useDiscountForm()
@@ -48,7 +50,10 @@ const AddTypeConditionSelector = ({ onClose }: AddConditionSelectorProps) => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search by type...",
searchPlaceholder: t(
"add-condition-tables-search-by-type",
"Search by type..."
),
}}
resourceName="Types"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminCollections } from "medusa-react"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -20,6 +21,7 @@ import DetailsConditionFooter from "./details-condition-footer"
const DetailsCollectionConditionSelector = ({
onClose,
}: AddConditionSelectorProps) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useConditions()
@@ -65,7 +67,10 @@ const DetailsCollectionConditionSelector = ({
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search by title...",
searchPlaceholder: t(
"details-condition-tables-search-by-title",
"Search by title..."
),
}}
resourceName="Collections"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminCustomerGroups } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -20,6 +21,7 @@ import DetailsConditionFooter from "./details-condition-footer"
const DetailsCustomerGroupConditionSelector = ({
onClose,
}: AddConditionSelectorProps) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useConditions()
@@ -59,7 +61,10 @@ const DetailsCustomerGroupConditionSelector = ({
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search groups...",
searchPlaceholder: t(
"details-condition-tables-search-groups",
"Search groups..."
),
}}
resourceName="Customer groups"
totalCount={count || 0}
@@ -1,4 +1,5 @@
import React, { useContext } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../../../../../components/fundamentals/button"
import { LayeredModalContext } from "../../../../../../components/molecules/modal/layered-modal"
import { useConditions } from "../../../../details/conditions/add-condition/conditions-provider"
@@ -22,13 +23,14 @@ const DetailsConditionFooter: React.FC<AddConditionFooterProps> = ({
operator,
onClose,
}) => {
const { t } = useTranslation()
const { pop, reset } = useContext(LayeredModalContext)
const { updateCondition, updateAndSave } = useConditions()
return (
<div className="gap-x-xsmall flex w-full justify-end">
<Button variant="ghost" size="small" onClick={onClose}>
Cancel
{t("details-condition-tables-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -42,7 +44,7 @@ const DetailsConditionFooter: React.FC<AddConditionFooterProps> = ({
pop()
}}
>
Save and add more
{t("details-condition-tables-save-and-add-more", "Save and add more")}
</Button>
<Button
variant="primary"
@@ -53,7 +55,7 @@ const DetailsConditionFooter: React.FC<AddConditionFooterProps> = ({
reset()
}}
>
Save and close
{t("details-condition-tables-save-and-close", "Save and close")}
</Button>
</div>
)
@@ -1,5 +1,6 @@
import { useAdminProducts } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -20,6 +21,7 @@ import DetailsConditionFooter from "./details-condition-footer"
const DetailsProductConditionSelector = ({
onClose,
}: AddConditionSelectorProps) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useConditions()
@@ -55,7 +57,10 @@ const DetailsProductConditionSelector = ({
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search products...",
searchPlaceholder: t(
"details-condition-tables-search-products",
"Search products..."
),
}}
resourceName="Products"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminProductTags } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -16,6 +17,7 @@ import DetailsConditionFooter from "./details-condition-footer"
const DetailsTagConditionSelector = ({
onClose,
}: AddConditionSelectorProps) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useConditions()
@@ -47,7 +49,10 @@ const DetailsTagConditionSelector = ({
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search by tag...",
searchPlaceholder: t(
"details-condition-tables-search-by-tag",
"Search by tag..."
),
}}
resourceName="Tags"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminProductTypes } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -16,6 +17,7 @@ import DetailsConditionFooter from "./details-condition-footer"
const DetailsTypeConditionSelector = ({
onClose,
}: AddConditionSelectorProps) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useConditions()
@@ -50,7 +52,10 @@ const DetailsTypeConditionSelector = ({
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search by type...",
searchPlaceholder: t(
"details-condition-tables-search-by-type",
"Search by type..."
),
}}
resourceName="Types"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminCollections } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -15,6 +16,7 @@ import ConditionOperator from "../shared/condition-operator"
import EditConditionFooter from "./edit-condition-footer"
const EditCollectionConditionSelector = ({ onClose }) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useDiscountForm()
const [items, setItems] = useState(
@@ -55,8 +57,16 @@ const EditCollectionConditionSelector = ({ onClose }) => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search by title...",
filters: [{ title: "Title", name: "title" }],
searchPlaceholder: t(
"edit-condition-tables-search-by-title",
"Search by title..."
),
filters: [
{
title: t("edit-condition-tables-title", "Title"),
name: "title",
},
],
}}
resourceName="Collections"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminCustomerGroups } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -15,6 +16,7 @@ import {
import EditConditionFooter from "./edit-condition-footer"
const EditCustomerGroupConditionSelector = ({ onClose }) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useDiscountForm()
const [items, setItems] = useState(conditions.customer_groups?.items || [])
@@ -52,7 +54,10 @@ const EditCustomerGroupConditionSelector = ({ onClose }) => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search groups...",
searchPlaceholder: t(
"edit-condition-tables-search-groups",
"Search groups..."
),
}}
resourceName="Customer groups"
totalCount={count || 0}
@@ -1,4 +1,5 @@
import React from "react"
import { useTranslation } from "react-i18next"
import Button from "../../../../../../components/fundamentals/button"
import { DiscountConditionOperator } from "../../../../types"
import { useDiscountForm } from "../../form/discount-form-context"
@@ -21,11 +22,12 @@ const EditConditionFooter: React.FC<EditConditionFooterProps> = ({
operator,
onClose,
}) => {
const { t } = useTranslation()
const { updateCondition } = useDiscountForm()
return (
<div className="gap-x-xsmall flex w-full items-center justify-end">
<Button variant="secondary" size="small" onClick={onClose}>
Cancel
{t("edit-condition-tables-cancel", "Cancel")}
</Button>
<Button
variant="danger"
@@ -39,7 +41,7 @@ const EditConditionFooter: React.FC<EditConditionFooterProps> = ({
onClose()
}}
>
Delete condition
{t("edit-condition-tables-delete-condition", "Delete condition")}
</Button>
<Button
variant="primary"
@@ -54,7 +56,7 @@ const EditConditionFooter: React.FC<EditConditionFooterProps> = ({
}}
className="min-w-[128px]"
>
Save
{t("edit-condition-tables-save", "Save")}
</Button>
</div>
)
@@ -1,6 +1,7 @@
import { Product } from "@medusajs/medusa"
import { useAdminProducts } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -16,6 +17,7 @@ import {
import EditConditionFooter from "./edit-condition-footer"
const EditProductConditionSelector = ({ onClose }) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useDiscountForm()
const [items, setItems] = useState(conditions.products?.items || [])
@@ -49,7 +51,10 @@ const EditProductConditionSelector = ({ onClose }) => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search products...",
searchPlaceholder: t(
"edit-condition-tables-search-products",
"Search products..."
),
}}
resourceName="Products"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminProductTags } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Spinner from "../../../../../../components/atoms/spinner"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
@@ -12,6 +13,7 @@ import { TagColumns, TagHeader, TagRow } from "../shared/tags"
import EditConditionFooter from "./edit-condition-footer"
const EditTagConditionSelector = ({ onClose }) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useDiscountForm()
@@ -46,7 +48,10 @@ const EditTagConditionSelector = ({ onClose }) => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search by tag...",
searchPlaceholder: t(
"edit-condition-tables-search-by-tag",
"Search by tag..."
),
}}
resourceName="Tags"
totalCount={count || 0}
@@ -1,5 +1,6 @@
import { useAdminProductTypes } from "medusa-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../../../components/molecules/modal"
import { SelectableTable } from "../../../../../../components/templates/selectable-table"
import useQueryFilters from "../../../../../../hooks/use-query-filters"
@@ -11,6 +12,7 @@ import { TypeRow, TypesHeader, useTypesColumns } from "../shared/types"
import EditConditionFooter from "./edit-condition-footer"
const EditTypeConditionSelector = ({ onClose }) => {
const { t } = useTranslation()
const params = useQueryFilters(defaultQueryProps)
const { conditions } = useDiscountForm()
const [items, setItems] = useState(conditions.product_types?.items || [])
@@ -43,7 +45,10 @@ const EditTypeConditionSelector = ({ onClose }) => {
options={{
enableSearch: true,
immediateSearchFocus: true,
searchPlaceholder: "Search by type...",
searchPlaceholder: t(
"edit-condition-tables-search-by-type",
"Search by type..."
),
}}
resourceName="Types"
totalCount={count || 0}
@@ -1,6 +1,7 @@
import { ProductCollection } from "@medusajs/medusa"
import { useMemo } from "react"
import { Column, HeaderGroup, Row } from "react-table"
import { useTranslation } from "react-i18next"
import SortingIcon from "../../../../../../components/fundamentals/icons/sorting-icon"
import Table from "../../../../../../components/molecules/table"
@@ -38,12 +39,13 @@ export const CollectionsHeader = ({
}
export const useCollectionColumns = () => {
const { t } = useTranslation()
const columns = useMemo<Column<ProductCollection>[]>(() => {
return [
{
Header: () => (
<div className="flex min-w-[546px] items-center gap-1">
Title <SortingIcon size={16} />
{t("shared-title", "Title")} <SortingIcon size={16} />
</div>
),
accessor: "title",
@@ -54,7 +56,7 @@ export const useCollectionColumns = () => {
{
Header: () => (
<div className="flex items-center justify-end gap-1">
Products <SortingIcon size={16} />
{t("shared-products", "Products")} <SortingIcon size={16} />
</div>
),
id: "products",
@@ -1,4 +1,5 @@
import React from "react"
import { useTranslation } from "react-i18next"
import RadioGroup from "../../../../../../components/organisms/radio-group"
import { DiscountConditionOperator } from "../../../../types"
@@ -11,6 +12,7 @@ const ConditionOperator: React.FC<ConditionOperatorProps> = ({
value,
onChange,
}) => {
const { t } = useTranslation()
return (
<RadioGroup.Root
value={value}
@@ -21,13 +23,19 @@ const ConditionOperator: React.FC<ConditionOperatorProps> = ({
className="w-full"
label="In"
value={DiscountConditionOperator.IN}
description="Applies to the selected items."
description={t(
"shared-applies-to-the-selected-items",
"Applies to the selected items."
)}
/>
<RadioGroup.Item
className="w-full"
label="Not in"
value={DiscountConditionOperator.NOT_IN}
description="Applies to all items except the selected items."
description={t(
"shared-applies-to-all-items-except-the-selected-items",
"Applies to all items except the selected items."
)}
/>
</RadioGroup.Root>
)
@@ -1,6 +1,7 @@
import { CustomerGroup } from "@medusajs/medusa"
import { useMemo } from "react"
import { Column, HeaderGroup, Row } from "react-table"
import { useTranslation } from "react-i18next"
import SortingIcon from "../../../../../../components/fundamentals/icons/sorting-icon"
import Table from "../../../../../../components/molecules/table"
@@ -38,12 +39,13 @@ export const CustomerGroupsRow = ({ row }: { row: Row<CustomerGroup> }) => {
}
export const useGroupColumns = () => {
const { t } = useTranslation()
const columns = useMemo<Column<CustomerGroup>[]>(() => {
return [
{
Header: () => (
<div className="flex min-w-[540px] items-center gap-1">
Title <SortingIcon size={16} />
{t("shared-title", "Title")} <SortingIcon size={16} />
</div>
),
accessor: "name",
@@ -51,7 +53,7 @@ export const useGroupColumns = () => {
{
Header: () => (
<div className="flex items-center justify-end gap-1">
Members <SortingIcon size={16} />
{t("shared-members", "Members")} <SortingIcon size={16} />
</div>
),
id: "members",
@@ -1,6 +1,7 @@
import { Product } from "@medusajs/medusa"
import { useMemo } from "react"
import { Column, HeaderGroup, Row } from "react-table"
import { useTranslation } from "react-i18next"
import SortingIcon from "../../../../../../components/fundamentals/icons/sorting-icon"
import ImagePlaceholder from "../../../../../../components/fundamentals/image-placeholder"
import StatusIndicator from "../../../../../../components/fundamentals/status-indicator"
@@ -54,12 +55,13 @@ export const ProductsHeader = ({
}
export const useProductColumns = () => {
const { t } = useTranslation()
const columns = useMemo<Column<Product>[]>(() => {
return [
{
Header: () => (
<div className="flex min-w-[443px] items-center gap-1">
Title <SortingIcon size={16} />
{t("shared-title", "Title")} <SortingIcon size={16} />
</div>
),
accessor: "title",
@@ -86,7 +88,7 @@ export const useProductColumns = () => {
{
Header: () => (
<div className="flex items-center gap-1">
Status <SortingIcon size={16} />
{t("shared-status", "Status")} <SortingIcon size={16} />
</div>
),
accessor: "status",
@@ -102,7 +104,7 @@ export const useProductColumns = () => {
{
Header: () => (
<div className="flex items-center justify-end gap-1">
Variants <SortingIcon size={16} />
{t("shared-variants", "Variants")} <SortingIcon size={16} />
</div>
),
id: "variants",
@@ -1,5 +1,6 @@
import { ProductTag } from "@medusajs/medusa"
import { Column, HeaderGroup, Row } from "react-table"
import { Translation } from "react-i18next"
import SortingIcon from "../../../../../../components/fundamentals/icons/sorting-icon"
import Table from "../../../../../../components/molecules/table"
@@ -7,7 +8,13 @@ export const TagColumns: Column<ProductTag>[] = [
{
Header: () => (
<div className="flex items-center gap-1">
Tag <SortingIcon size={16} />
<Translation>
{(t) => (
<>
{t("shared-tag", "Tag")} <SortingIcon size={16} />
</>
)}
</Translation>
</div>
),
accessor: "value",
@@ -1,6 +1,7 @@
import { ProductType } from "@medusajs/medusa"
import { useMemo } from "react"
import { Column, HeaderGroup, Row } from "react-table"
import { Translation } from "react-i18next"
import SortingIcon from "../../../../../../components/fundamentals/icons/sorting-icon"
import Table from "../../../../../../components/molecules/table"
@@ -42,9 +43,13 @@ export const useTypesColumns = () => {
return [
{
Header: () => (
<div className="flex min-w-[626px] items-center gap-1">
Type <SortingIcon size={16} />
</div>
<Translation>
{(t) => (
<div className="flex min-w-[626px] items-center gap-1">
{t("shared-type", "Type")} <SortingIcon size={16} />
</div>
)}
</Translation>
),
accessor: "value",
Cell: ({ row: { original } }) => {
@@ -1,4 +1,5 @@
import React from "react"
import { useTranslation } from "react-i18next"
import Modal from "../../../../components/molecules/modal"
import { DiscountConditionType } from "../../types"
import { getTitle } from "../../utils"
@@ -17,10 +18,15 @@ const EditConditionsModal: React.FC<EditConditionsModalProps> = ({
onClose,
view,
}) => {
const { t } = useTranslation()
return (
<Modal open handleClose={onClose}>
<Modal.Header handleClose={onClose}>
<h1 className="inter-xlarge-semibold">Edit {getTitle(view)}</h1>
<h1 className="inter-xlarge-semibold">
{t("edit-conditions-modal-title", "Edit {{title}}", {
title: getTitle(view, t),
})}
</h1>
</Modal.Header>
<Modal.Body>
<Content view={view} onClose={onClose} />
@@ -5,6 +5,7 @@ import {
useFormContext,
UseFormReturn,
} from "react-hook-form"
import {
AllocationType,
ConditionMap,
@@ -14,6 +15,7 @@ import {
UpdateConditionProps,
} from "../../../types"
import { DiscountFormValues } from "./mappers"
import { useTranslation } from "react-i18next"
type DiscountFormProviderProps = {
children?: React.ReactNode
@@ -208,8 +210,15 @@ const DiscountFormContext = React.createContext<{
export const useDiscountForm = () => {
const context = React.useContext(DiscountFormContext)
const form = useFormContext<DiscountFormValues>()
const { t } = useTranslation()
if (!context) {
throw new Error("useDiscountForm must be a child of DiscountFormContext")
throw new Error(
t(
"form-use-discount-form-must-be-a-child-of-discount-form-context",
"useDiscountForm must be a child of DiscountFormContext"
)
)
}
return { ...form, ...context }
}
@@ -1,5 +1,6 @@
import { useWatch } from "react-hook-form"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import MetadataForm from "../../../../components/forms/general/metadata-form"
import Button from "../../../../components/fundamentals/button"
import CrossIcon from "../../../../components/fundamentals/icons/cross-icon"
@@ -26,6 +27,7 @@ const DiscountForm = ({ closeForm }: DiscountFormProps) => {
const navigate = useNavigate()
const notification = useNotification()
const { handleSubmit, handleReset, control, form } = useDiscountForm()
const { t } = useTranslation()
const { onSaveAsActive, onSaveAsInactive } = useFormActions()
@@ -45,7 +47,11 @@ const DiscountForm = ({ closeForm }: DiscountFormProps) => {
handleReset()
})
.catch((error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("discount-form-error", "Error"),
getErrorMessage(error),
"error"
)
})
}
@@ -55,7 +61,11 @@ const DiscountForm = ({ closeForm }: DiscountFormProps) => {
closeFormModal()
handleReset()
} catch (error) {
notification("Error", getErrorMessage(error), "error")
notification(
t("discount-form-error", "Error"),
getErrorMessage(error),
"error"
)
}
}
@@ -83,7 +93,7 @@ const DiscountForm = ({ closeForm }: DiscountFormProps) => {
variant="ghost"
className="rounded-rounded border"
>
Save as draft
{t("discount-form-save-as-draft", "Save as draft")}
</Button>
<Button
size="small"
@@ -91,7 +101,7 @@ const DiscountForm = ({ closeForm }: DiscountFormProps) => {
onClick={handleSubmit(submitCTA)}
className="rounded-rounded"
>
Publish discount
{t("discount-form-publish-discount", "Publish discount")}
</Button>
</div>
</div>
@@ -99,7 +109,9 @@ const DiscountForm = ({ closeForm }: DiscountFormProps) => {
<FocusModal.Main>
<div className="mb-[25%] flex justify-center">
<div className="w-full max-w-[700px] pt-16">
<h1 className="inter-xlarge-semibold">Create new discount</h1>
<h1 className="inter-xlarge-semibold">
{t("discount-form-create-new-discount", "Create new discount")}
</h1>
<Accordion
className="text-grey-90 pt-7"
defaultValue={["promotion-type"]}
@@ -107,23 +119,27 @@ const DiscountForm = ({ closeForm }: DiscountFormProps) => {
>
<Accordion.Item
forceMountContent
title="Discount type"
title={t("discount-form-discount-type", "Discount type")}
required
tooltip="Select a discount type"
tooltip={t(
"discount-form-select-a-discount-type",
"Select a discount type"
)}
value="promotion-type"
>
<DiscountType />
{discountType === DiscountRuleType.FIXED && (
<div className="mt-xlarge">
<h3 className="inter-base-semibold">
Allocation<span className="text-rose-50">*</span>
{t("discount-form-allocation", "Allocation")}
<span className="text-rose-50">*</span>
</h3>
<DiscountAllocation />
</div>
)}
</Accordion.Item>
<Accordion.Item
title="General"
title={t("discount-form-general", "General")}
required
value="general"
forceMountContent
@@ -132,24 +148,36 @@ const DiscountForm = ({ closeForm }: DiscountFormProps) => {
</Accordion.Item>
<Accordion.Item
forceMountContent
title="Configuration"
title={t("discount-form-configuration", "Configuration")}
value="configuration"
description="Discount code applies from you hit the publish button and forever if left untouched."
description={t(
"discount-form-discount-code-application-disclaimer",
"Discount code applies from when you hit the publish button and forever if left untouched."
)}
>
<Configuration />
</Accordion.Item>
<Accordion.Item
forceMountContent
title="Conditions"
description="Discount code apply to all products if left untouched."
title={t("discount-form-conditions", "Conditions")}
description={t(
"discount-form-discount-code-apply-to-all-products-if-left-untouched",
"Discount code apply to all products if left untouched."
)}
value="conditions"
tooltip="Add conditions to your Discount"
tooltip={t(
"discount-form-add-conditions-to-your-discount",
"Add conditions to your Discount"
)}
>
<DiscountNewConditions />
</Accordion.Item>
<Accordion.Item
title="Metadata"
subtitle="Metadata allows you to add additional information to your discount."
title={t("discount-form-metadata", "Metadata")}
subtitle={t(
"discount-form-metadata-usage-description",
"Metadata allows you to add additional information to your discount."
)}
value="metadata"
forceMountContent
>
@@ -11,6 +11,8 @@ import {
} from "../../../../types"
import EditConditionsModal from "../../edit-conditions-modal"
import { useDiscountForm } from "../../form/discount-form-context"
import { useTranslation } from "react-i18next"
import { TFunction } from "i18next"
type ConditionItemProps<Type extends DiscountConditionType> = {
index: number
@@ -29,6 +31,7 @@ const ConditionItem = <Type extends DiscountConditionType>({
setCondition,
items,
}: ConditionItemProps<Type>) => {
const { t } = useTranslation()
const queryParams = useMemo(() => {
switch (type) {
case DiscountConditionType.PRODUCTS:
@@ -173,7 +176,7 @@ const ConditionItem = <Type extends DiscountConditionType>({
</Badge>
</div>
<div className="flex w-full flex-1 flex-col justify-center truncate">
<div className="inter-small-semibold">{getTitle(type)}</div>
<div className="inter-small-semibold">{getTitle(type, t)}</div>
<div className="inter-small-regular gap-x-xsmall flex w-full flex-1 items-center">
<div className="gap-x-2xsmall text-grey-50 inter-small-regular flex w-full flex-1 items-center">
{visibleItems.map((item, i) => {
@@ -186,7 +189,11 @@ const ConditionItem = <Type extends DiscountConditionType>({
)
})}
{remainder > 0 && (
<span className="text-grey-40 ml-2">+{remainder} more</span>
<span className="text-grey-40 ml-2">
{t("condition-item-remainder-more", "+{{remainder}} more", {
remainder,
})}
</span>
)}
</div>
</div>
@@ -197,12 +204,12 @@ const ConditionItem = <Type extends DiscountConditionType>({
forceDropdown
actions={[
{
label: "Edit",
label: t("conditions-edit", "Edit"),
onClick: () => setShowEdit(true),
icon: <EditIcon size={16} />,
},
{
label: "Delete condition",
label: t("conditions-delete-condition", "Delete condition"),
onClick: () =>
updateCondition({
type,
@@ -223,18 +230,18 @@ const ConditionItem = <Type extends DiscountConditionType>({
)
}
const getTitle = (type: DiscountConditionType) => {
const getTitle = (type: DiscountConditionType, t: TFunction) => {
switch (type) {
case DiscountConditionType.PRODUCTS:
return "Product"
return t("conditions-product", "Product")
case DiscountConditionType.PRODUCT_COLLECTIONS:
return "Collection"
return t("conditions-collection", "Collection")
case DiscountConditionType.PRODUCT_TAGS:
return "Tag"
return t("conditions-tag", "Tag")
case DiscountConditionType.CUSTOMER_GROUPS:
return "Customer group"
return t("conditions-customer-group", "Customer group")
case DiscountConditionType.PRODUCT_TYPES:
return "Type"
return t("conditions-type", "Type")
}
}
@@ -1,5 +1,6 @@
import { Discount } from "@medusajs/medusa"
import React, { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../../../../../components/fundamentals/button"
import PlusIcon from "../../../../../../components/fundamentals/icons/plus-icon"
import AddConditionsModal from "../../add-conditions-modal"
@@ -13,6 +14,7 @@ type DiscountNewConditionsProps = {
const DiscountNewConditions: React.FC<DiscountNewConditionsProps> = ({
discount,
}) => {
const { t } = useTranslation()
const { setConditions, conditions } = useDiscountForm()
const [showConditionsModal, setShowConditionsModal] = useState(false)
@@ -68,7 +70,7 @@ const DiscountNewConditions: React.FC<DiscountNewConditionsProps> = ({
className="rounded-rounded mt-4 w-full border p-2"
>
<PlusIcon size={18} />
<span>Add Condition</span>
<span>{t("conditions-add-condition", "Add Condition")}</span>
</Button>
)}
{showConditionsModal && (
@@ -2,6 +2,7 @@ import { Discount } from "@medusajs/medusa"
import clsx from "clsx"
import React, { useEffect, useState } from "react"
import { Controller } from "react-hook-form"
import { useTranslation } from "react-i18next"
import DatePicker from "../../../../../components/atoms/date-picker/date-picker"
import TimePicker from "../../../../../components/atoms/date-picker/time-picker"
import Switch from "../../../../../components/atoms/switch"
@@ -45,6 +46,7 @@ const Settings: React.FC<SettingsProps> = ({ promotion, isEdit = false }) => {
hasExpiryDate,
handleConfigurationChanged,
} = useDiscountForm()
const { t } = useTranslation()
const [openItems, setOpenItems] = React.useState<string[]>(
isEdit && promotion
@@ -78,9 +80,15 @@ const Settings: React.FC<SettingsProps> = ({ promotion, isEdit = false }) => {
headingSize="medium"
forceMountContent
className="border-b-0"
title="Start date"
subtitle="Schedule the discount to activate in the future."
tooltip="If you want to schedule the discount to activate in the future, you can set a start date here, otherwise the discount will be active immediately."
title={t("sections-start-date", "Start date")}
subtitle={t(
"sections-schedule-the-discount-to-activate-in-the-future",
"Schedule the discount to activate in the future."
)}
tooltip={t(
"sections-select-discount-start-date",
"If you want to schedule the discount to activate in the future, you can set a start date here, otherwise the discount will be active immediately."
)}
value="starts_at"
customTrigger={
<Switch checked={openItems.indexOf("starts_at") > -1} />
@@ -104,11 +112,11 @@ const Settings: React.FC<SettingsProps> = ({ promotion, isEdit = false }) => {
<>
<DatePicker
date={date}
label="Start date"
label={t("sections-start-date", "Start date")}
onSubmitDate={onChange}
/>
<TimePicker
label="Start time"
label={t("sections-start-time", "Start time")}
date={date}
onSubmitDate={onChange}
/>
@@ -122,9 +130,18 @@ const Settings: React.FC<SettingsProps> = ({ promotion, isEdit = false }) => {
headingSize="medium"
forceMountContent
className="border-b-0"
title="Discount has an expiry date?"
subtitle="Schedule the discount to deactivate in the future."
tooltip="If you want to schedule the discount to deactivate in the future, you can set an expiry date here."
title={t(
"sections-discount-has-an-expiry-date",
"Discount has an expiry date?"
)}
subtitle={t(
"sections-schedule-the-discount-to-deactivate-in-the-future",
"Schedule the discount to deactivate in the future."
)}
tooltip={t(
"sections-select-discount-end-date",
"If you want to schedule the discount to deactivate in the future, you can set an expiry date here."
)}
value="ends_at"
customTrigger={
<Switch checked={openItems.indexOf("ends_at") > -1} />
@@ -150,11 +167,11 @@ const Settings: React.FC<SettingsProps> = ({ promotion, isEdit = false }) => {
<>
<DatePicker
date={date}
label="Expiry date"
label={t("sections-expiry-date", "Expiry date")}
onSubmitDate={onChange}
/>
<TimePicker
label="Expiry time"
label={t("sections-expiry-time", "Expiry time")}
date={date}
onSubmitDate={onChange}
/>
@@ -168,9 +185,18 @@ const Settings: React.FC<SettingsProps> = ({ promotion, isEdit = false }) => {
headingSize="medium"
forceMountContent
className="border-b-0"
title="Limit the number of redemptions?"
subtitle="Limit applies across all customers, not per customer."
tooltip="If you wish to limit the amount of times a customer can redeem this discount, you can set a limit here."
title={t(
"sections-limit-the-number-of-redemptions",
"Limit the number of redemptions?"
)}
subtitle={t(
"sections-limit-applies-across-all-customers-not-per-customer",
"Limit applies across all customers, not per customer."
)}
tooltip={t(
"sections-limit-discount-number-of-uses",
"If you wish to limit the amount of times a customer can redeem this discount, you can set a limit here."
)}
value="usage_limit"
customTrigger={
<Switch checked={openItems.indexOf("usage_limit") > -1} />
@@ -183,7 +209,10 @@ const Settings: React.FC<SettingsProps> = ({ promotion, isEdit = false }) => {
>
<InputField
{...register("usage_limit", { valueAsNumber: true })}
label="Number of redemptions"
label={t(
"sections-number-of-redemptions",
"Number of redemptions"
)}
type="number"
placeholder="5"
min={1}
@@ -196,10 +225,19 @@ const Settings: React.FC<SettingsProps> = ({ promotion, isEdit = false }) => {
disabled={!isDynamic}
headingSize="medium"
forceMountContent
title="Availability duration?"
title={t(
"sections-availability-duration",
"Availability duration?"
)}
className="border-b-0"
subtitle="Set the duration of the discount."
tooltip="Select a discount type"
subtitle={t(
"sections-set-the-duration-of-the-discount",
"Set the duration of the discount."
)}
tooltip={t(
"sections-select-a-discount-type",
"Select a discount type"
)}
value="valid_duration"
customTrigger={
<Switch checked={openItems.indexOf("valid_duration") > -1} />
@@ -1,11 +1,13 @@
import clsx from "clsx"
import { Controller } from "react-hook-form"
import { useTranslation } from "react-i18next"
import RadioGroup from "../../../../../components/organisms/radio-group"
import { AllocationType } from "../../../types"
import { useDiscountForm } from "../form/discount-form-context"
const DiscountAllocation = () => {
const { control } = useDiscountForm()
const { t } = useTranslation()
return (
<Controller
@@ -22,14 +24,20 @@ const DiscountAllocation = () => {
<RadioGroup.Item
value={AllocationType.TOTAL}
className="flex-1"
label="Total amount"
description="Apply to the total amount"
label={t("sections-total-amount", "Total amount")}
description={t(
"sections-apply-to-the-total-amount",
"Apply to the total amount"
)}
/>
<RadioGroup.Item
value={AllocationType.ITEM}
className="flex-1"
label="Item specific"
description="Apply to every allowed item"
label={t("sections-item-specific", "Item specific")}
description={t(
"sections-apply-to-every-allowed-item",
"Apply to every allowed item"
)}
/>
</RadioGroup.Root>
)
@@ -1,11 +1,13 @@
import clsx from "clsx"
import { Controller, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import RadioGroup from "../../../../../components/organisms/radio-group"
import { DiscountRuleType } from "../../../types"
import { useDiscountForm } from "../form/discount-form-context"
const DiscountType = () => {
const { control } = useDiscountForm()
const { t } = useTranslation()
const regions = useWatch({
control,
@@ -27,22 +29,31 @@ const DiscountType = () => {
<RadioGroup.Item
value={DiscountRuleType.PERCENTAGE}
className="flex-1"
label="Percentage"
label={t("sections-percentage", "Percentage")}
description={"Discount applied in %"}
/>
<RadioGroup.Item
value={DiscountRuleType.FIXED}
className="flex-1"
label="Fixed amount"
description={"Discount in whole numbers"}
label={t("sections-fixed-amount", "Fixed amount")}
description={t(
"sections-discount-in-whole-numbers",
"Discount in whole numbers"
)}
disabled={Array.isArray(regions) && regions.length > 1}
disabledTooltip="You can only select one valid region if you want to use the fixed amount type"
disabledTooltip={t(
"sections-you-can-only-select-one-valid-region-if-you-want-to-use-the-fixed-amount-type",
"You can only select one valid region if you want to use the fixed amount type"
)}
/>
<RadioGroup.Item
value={DiscountRuleType.FREE_SHIPPING}
className="flex-1"
label="Free shipping"
description={"Override delivery amount"}
label={t("sections-free-shipping", "Free shipping")}
description={t(
"sections-override-delivery-amount",
"Override delivery amount"
)}
/>
</RadioGroup.Root>
)
@@ -2,6 +2,7 @@ import { Discount } from "@medusajs/medusa"
import { useAdminRegions } from "medusa-react"
import React, { useEffect, useMemo, useState } from "react"
import { Controller, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import Checkbox from "../../../../../components/atoms/checkbox"
import IconTooltip from "../../../../../components/molecules/icon-tooltip"
import InputField from "../../../../../components/molecules/input"
@@ -17,6 +18,7 @@ type GeneralProps = {
const General: React.FC<GeneralProps> = ({ discount }) => {
const initialCurrency = discount?.regions?.[0].currency_code || undefined
const { t } = useTranslation()
const [fixedRegionCurrency, setFixedRegionCurrency] = useState<
string | undefined
>(initialCurrency)
@@ -59,7 +61,10 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
name="regions"
control={control}
rules={{
required: "At least one region is required",
required: t(
"sections-at-least-one-region-is-required",
"At least one region is required"
),
validate: (value) =>
Array.isArray(value) ? value.length > 0 : !!value,
}}
@@ -70,7 +75,10 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
onChange={(value) => {
onChange(type === "fixed" ? [value] : value)
}}
label="Choose valid regions"
label={t(
"sections-choose-valid-regions",
"Choose valid regions"
)}
isMulti={type !== "fixed"}
selectAll={type !== "fixed"}
isSearchable
@@ -82,11 +90,13 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
/>
<div className="gap-x-base gap-y-base my-base flex">
<InputField
label="Code"
label={t("sections-code", "Code")}
className="flex-1"
placeholder="SUMMERSALE10"
placeholder={t("sections-summersale-10", "SUMMERSALE10")}
required
{...register("code", { required: "Code is required" })}
{...register("code", {
required: t("sections-code-is-required", "Code is required"),
})}
/>
{type !== "free_shipping" && (
@@ -103,13 +113,16 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
name="rule.value"
control={control}
rules={{
required: "Amount is required",
required: t(
"sections-amount-is-required",
"Amount is required"
),
min: 1,
}}
render={({ field: { value, onChange } }) => {
return (
<CurrencyInput.Amount
label={"Amount"}
label={t("sections-amount", "Amount")}
required
amount={value}
onChange={onChange}
@@ -122,7 +135,7 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
) : (
<div className="flex-1">
<InputField
label="Percentage"
label={t("sections-percentage", "Percentage")}
min={0}
required
type="number"
@@ -141,15 +154,22 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
<div className="text-grey-50 inter-small-regular mb-6 flex flex-col">
<span>
The code your customers will enter during checkout. This will
appear on your customers invoice.
{t(
"sections-customer-invoice-code",
"The code your customers will enter during checkout. This will appear on your customer\u2019s invoice."
)}
</span>
<span>
{t(
"sections-uppercase-letters-and-numbers-only",
"Uppercase letters and numbers only."
)}
</span>
<span>Uppercase letters and numbers only.</span>
</div>
<TextArea
label="Description"
label={t("sections-description", "Description")}
required
placeholder="Summer Sale 2022"
placeholder={t("sections-summer-sale-2022", "Summer Sale 2022")}
rows={1}
{...register("rule.description", {
required: true,
@@ -162,7 +182,10 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
render={({ field: { onChange, value } }) => {
return (
<Checkbox
label="This is a template discount"
label={t(
"sections-this-is-a-template-discount",
"This is a template discount"
)}
name="is_dynamic"
id="is_dynamic"
checked={value}
@@ -172,9 +195,10 @@ const General: React.FC<GeneralProps> = ({ discount }) => {
}}
/>
<IconTooltip
content={
content={t(
"sections-template-discounts-description",
"Template discounts allow you to define a set of rules that can be used across a group of discounts. This is useful in campaigns that should generate unique codes for each user, but where the rules for all unique codes should be the same."
}
)}
/>
</div>
</>
@@ -1,4 +1,5 @@
import { useContext, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { LayeredModalContext } from "../../../../components/molecules/modal/layered-modal"
import { DiscountConditionType } from "../../types"
import AddCollectionConditionSelector from "./condition-tables/add-condition-tables/collections"
@@ -29,16 +30,20 @@ const useConditionModalItems = ({
onClose,
}: UseConditionModalItemsProps) => {
const layeredModalContext = useContext(LayeredModalContext)
const { t } = useTranslation()
const items: ConditionItem[] = useMemo(
() => [
{
label: "Product",
label: t("discount-form-product", "Product"),
value: DiscountConditionType.PRODUCTS,
description: "Only for specific products",
description: t(
"discount-form-only-for-specific-products",
"Only for specific products"
),
onClick: () =>
layeredModalContext.push({
title: "Choose products",
title: t("discount-form-choose-products", "Choose products"),
onBack: () => layeredModalContext.pop(),
view: isDetails ? (
<DetailsProductConditionSelector onClose={onClose} />
@@ -48,12 +53,15 @@ const useConditionModalItems = ({
}),
},
{
label: "Customer group",
label: t("discount-form-customer-group", "Customer group"),
value: DiscountConditionType.CUSTOMER_GROUPS,
description: "Only for specific customer groups",
description: t(
"discount-form-only-for-specific-customer-groups",
"Only for specific customer groups"
),
onClick: () => {
layeredModalContext.push({
title: "Choose groups",
title: t("discount-form-choose-groups", "Choose groups"),
onBack: () => layeredModalContext.pop(),
view: isDetails ? (
<DetailsCustomerGroupConditionSelector onClose={onClose} />
@@ -64,9 +72,12 @@ const useConditionModalItems = ({
},
},
{
label: "Tag",
label: t("discount-form-tag", "Tag"),
value: DiscountConditionType.PRODUCT_TAGS,
description: "Only for specific tags",
description: t(
"discount-form-only-for-specific-tags",
"Only for specific tags"
),
onClick: () =>
layeredModalContext.push({
title: "Choose tags",
@@ -79,12 +90,15 @@ const useConditionModalItems = ({
}),
},
{
label: "Collection",
label: t("discount-form-collection", "Collection"),
value: DiscountConditionType.PRODUCT_COLLECTIONS,
description: "Only for specific product collections",
description: t(
"discount-form-only-for-specific-product-collections",
"Only for specific product collections"
),
onClick: () =>
layeredModalContext.push({
title: "Choose collections",
title: t("discount-form-choose-collections", "Choose collections"),
onBack: () => layeredModalContext.pop(),
view: isDetails ? (
<DetailsCollectionConditionSelector onClose={onClose} />
@@ -94,12 +108,15 @@ const useConditionModalItems = ({
}),
},
{
label: "Type",
label: t("discount-form-type", "Type"),
value: DiscountConditionType.PRODUCT_TYPES,
description: "Only for specific product types",
description: t(
"discount-form-only-for-specific-product-types",
"Only for specific product types"
),
onClick: () =>
layeredModalContext.push({
title: "Choose types",
title: t("discount-form-choose-types", "Choose types"),
onBack: () => layeredModalContext.pop(),
view: isDetails ? (
<DetailsTypeConditionSelector onClose={onClose} />
@@ -1,3 +1,5 @@
import { TFunction } from "i18next"
enum DiscountConditionType {
PRODUCTS = "products",
PRODUCT_TYPES = "product_types",
@@ -6,17 +8,17 @@ enum DiscountConditionType {
CUSTOMER_GROUPS = "customer_groups",
}
export const getTitle = (view: DiscountConditionType) => {
export const getTitle = (view: DiscountConditionType, t: TFunction) => {
switch (view) {
case DiscountConditionType.PRODUCTS:
return "products"
return t("utils-products", "products")
case DiscountConditionType.CUSTOMER_GROUPS:
return "groups"
return t("utils-groups", "groups")
case DiscountConditionType.PRODUCT_TAGS:
return "tags"
return t("utils-tags", "tags")
case DiscountConditionType.PRODUCT_COLLECTIONS:
return "collections"
return t("utils-collections", "collections")
case DiscountConditionType.PRODUCT_TYPES:
return "types"
return t("utils-types", "types")
}
}
@@ -1,6 +1,7 @@
import { useAdminCreateGiftCard } from "medusa-react"
import React, { useEffect } from "react"
import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import GiftCardBalanceForm, {
GiftCardBalanceFormType,
} from "../../components/forms/gift-card/gift-card-balance-form"
@@ -32,6 +33,7 @@ type CustomGiftCardFormType = {
}
const CustomGiftcard: React.FC<CustomGiftcardProps> = ({ onClose, open }) => {
const { t } = useTranslation()
const form = useForm<CustomGiftCardFormType>()
const {
handleSubmit,
@@ -70,14 +72,21 @@ const CustomGiftcard: React.FC<CustomGiftcardProps> = ({ onClose, open }) => {
{
onSuccess: () => {
notification(
"Created gift card",
"Custom gift card was created successfully",
t("gift-cards-created-gift-card", "Created gift card"),
t(
"gift-cards-custom-gift-card-was-created-successfully",
"Custom gift card was created successfully"
),
"success"
)
onClose()
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("gift-cards-error", "Error"),
getErrorMessage(error),
"error"
)
},
}
)
@@ -87,13 +96,17 @@ const CustomGiftcard: React.FC<CustomGiftcardProps> = ({ onClose, open }) => {
<Modal open={open} handleClose={onClose}>
<Modal.Body>
<Modal.Header handleClose={onClose}>
<h2 className="inter-xlarge-semibold">Custom Gift Card</h2>
<h2 className="inter-xlarge-semibold">
{t("gift-cards-custom-gift-card", "Custom Gift Card")}
</h2>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
<div className="gap-y-xlarge flex flex-col">
<div>
<h2 className="inter-base-semibold mb-base">Details</h2>
<h2 className="inter-base-semibold mb-base">
{t("gift-cards-details", "Details")}
</h2>
<div className="gap-x-xsmall grid grid-cols-2">
<GiftCardRegionForm form={nestedForm(form, "region")} />
<GiftCardBalanceForm
@@ -104,7 +117,9 @@ const CustomGiftcard: React.FC<CustomGiftcardProps> = ({ onClose, open }) => {
</div>
<GiftCardEndsAtForm form={nestedForm(form, "ends_at")} />
<div>
<h2 className="inter-base-semibold mb-base">Receiver</h2>
<h2 className="inter-base-semibold mb-base">
{t("gift-cards-receiver", "Receiver")}
</h2>
<GiftCardReceiverForm form={nestedForm(form, "receiver")} />
</div>
</div>
@@ -117,7 +132,7 @@ const CustomGiftcard: React.FC<CustomGiftcardProps> = ({ onClose, open }) => {
size="small"
type="button"
>
Cancel
{t("gift-cards-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -126,7 +141,7 @@ const CustomGiftcard: React.FC<CustomGiftcardProps> = ({ onClose, open }) => {
disabled={isSubmitting || !isDirty}
loading={isSubmitting}
>
Create and send
{t("gift-cards-create-and-send", "Create and send")}
</Button>
</div>
</Modal.Footer>
@@ -2,6 +2,7 @@ import { GiftCard } from "@medusajs/medusa"
import { useAdminUpdateGiftCard } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import GiftCardEndsAtForm, {
GiftCardEndsAtFormType,
} from "../../../components/forms/gift-card/gift-card-ends-at-form"
@@ -30,6 +31,7 @@ const EditGiftCardModal = ({
onClose,
giftCard,
}: EditGiftCardModalProps) => {
const { t } = useTranslation()
const form = useForm<EditGiftCardFormType>({
defaultValues: getDefaultValues(giftCard),
})
@@ -52,8 +54,11 @@ const EditGiftCardModal = ({
{
onSuccess: () => {
notification(
"Updated Gift card",
"Gift card was successfully updated",
t("details-updated-gift-card", "Updated Gift card"),
t(
"details-gift-card-was-successfully-updated",
"Gift card was successfully updated"
),
"success"
)
@@ -61,7 +66,10 @@ const EditGiftCardModal = ({
},
onError: (err) => {
notification(
"Failed to update Gift card",
t(
"details-failed-to-update-gift-card",
"Failed to update Gift card"
),
getErrorMessage(err),
"error"
)
@@ -80,13 +88,17 @@ const EditGiftCardModal = ({
<Modal open={open} handleClose={onClose}>
<Modal.Body>
<Modal.Header handleClose={onClose}>
<h1 className="inter-xlarge-semibold">Edit Gift Card</h1>
<h1 className="inter-xlarge-semibold">
{t("details-edit-gift-card", "Edit Gift Card")}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
<div className="gap-y-xlarge flex flex-col">
<div>
<h2 className="inter-base-semibold mb-base">Details</h2>
<h2 className="inter-base-semibold mb-base">
{t("details-details", "Details")}
</h2>
<GiftCardRegionForm form={nestedForm(form, "region")} />
</div>
<GiftCardEndsAtForm form={nestedForm(form, "ends_at")} />
@@ -100,7 +112,7 @@ const EditGiftCardModal = ({
onClick={onClose}
type="button"
>
Cancel
{t("details-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -109,7 +121,7 @@ const EditGiftCardModal = ({
disabled={isLoading || !isDirty}
loading={isLoading}
>
Save and close
{t("details-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>
@@ -16,9 +16,11 @@ import { getErrorMessage } from "../../../utils/error-messages"
import { formatAmountWithSymbol } from "../../../utils/prices"
import EditGiftCardModal from "./edit-gift-card-modal"
import UpdateBalanceModal from "./update-balance-modal"
import { useTranslation } from "react-i18next"
const GiftCardDetails = () => {
const { id } = useParams()
const { t } = useTranslation()
const { gift_card: giftCard, isLoading } = useAdminGiftCard(id!, {
enabled: !!id,
@@ -44,12 +46,12 @@ const GiftCardDetails = () => {
const actions = [
{
label: "Edit details",
label: t("details-edit-details", "Edit details"),
onClick: openEdit,
icon: <EditIcon size={20} />,
},
{
label: "Update balance",
label: t("details-update-balance-label", "Update balance"),
onClick: openBalance,
icon: <DollarSignIcon size={20} />,
},
@@ -61,12 +63,20 @@ const GiftCardDetails = () => {
{
onSuccess: () => {
notification(
"Updated status",
"Successfully updated the status of the Gift Card",
t("details-updated-status", "Updated status"),
t(
"details-successfully-updated-the-status-of-the-gift-card",
"Successfully updated the status of the Gift Card"
),
"success"
)
},
onError: (err) => notification("Error", getErrorMessage(err), "error"),
onError: (err) =>
notification(
t("details-error", "Error"),
getErrorMessage(err),
"error"
),
}
)
}
@@ -74,7 +84,7 @@ const GiftCardDetails = () => {
return (
<div>
<BackButton
label="Back to Gift Cards"
label={t("details-back-to-gift-cards", "Back to Gift Cards")}
path="/a/gift-cards"
className="mb-xsmall"
/>
@@ -115,7 +125,7 @@ const GiftCardDetails = () => {
<div className="flex space-x-6 divide-x">
<div className="flex flex-col">
<div className="inter-smaller-regular text-grey-50 mb-1">
Original amount
{t("details-original-amount", "Original amount")}
</div>
<div>
{formatAmountWithSymbol({
@@ -126,7 +136,7 @@ const GiftCardDetails = () => {
</div>
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Balance
{t("details-balance", "Balance")}
</div>
<div>
{formatAmountWithSymbol({
@@ -137,14 +147,14 @@ const GiftCardDetails = () => {
</div>
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Region
{t("details-region", "Region")}
</div>
<div>{giftCard.region.name}</div>
</div>
{giftCard.ends_at && (
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Expires on
{t("details-expires-on", "Expires on")}
</div>
<div>
{moment(giftCard.ends_at).format("DD MMM YYYY")}
@@ -153,7 +163,7 @@ const GiftCardDetails = () => {
)}
<div className="flex flex-col pl-6">
<div className="inter-smaller-regular text-grey-50 mb-1">
Created
{t("details-created", "Created")}
</div>
<div>
{moment(giftCard.created_at).format("DD MMM YYYY")}
@@ -174,7 +184,10 @@ const GiftCardDetails = () => {
)
})}
<RawJSON data={giftCard} title="Raw gift card" />
<RawJSON
data={giftCard}
title={t("details-raw-gift-card", "Raw gift card")}
/>
</div>
<UpdateBalanceModal
@@ -2,6 +2,7 @@ import { GiftCard } from "@medusajs/medusa"
import { useAdminUpdateGiftCard } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import GiftCardBalanceForm, {
GiftCardBalanceFormType,
} from "../../../components/forms/gift-card/gift-card-balance-form"
@@ -26,6 +27,7 @@ const UpdateBalanceModal = ({
onClose,
giftCard,
}: UpdateBalanceModalProps) => {
const { t } = useTranslation()
const form = useForm<UpdateBalanceModalFormData>({
defaultValues: getDefaultValues(giftCard),
})
@@ -48,8 +50,11 @@ const UpdateBalanceModal = ({
{
onSuccess: () => {
notification(
"Balance updated",
"Gift card balance was updated",
t("details-balance-updated", "Balance updated"),
t(
"details-gift-card-balance-was-updated",
"Gift card balance was updated"
),
"success"
)
@@ -57,7 +62,7 @@ const UpdateBalanceModal = ({
},
onError: (err) => {
notification(
"Failed to update balance",
t("details-failed-to-update-balance", "Failed to update balance"),
getErrorMessage(err),
"error"
)
@@ -76,7 +81,9 @@ const UpdateBalanceModal = ({
<Modal open={open} handleClose={onClose}>
<Modal.Body>
<Modal.Header handleClose={onClose}>
<h1 className="inter-xlarge-semibold">Update Balance</h1>
<h1 className="inter-xlarge-semibold">
{t("details-update-balance", "Update Balance")}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
@@ -94,7 +101,7 @@ const UpdateBalanceModal = ({
onClick={onClose}
type="button"
>
Cancel
{t("details-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -103,7 +110,7 @@ const UpdateBalanceModal = ({
loading={isLoading}
disabled={isLoading || !isDirty}
>
Save and close
{t("details-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>
@@ -1,6 +1,7 @@
import { Product } from "@medusajs/medusa"
import { useAdminProducts } from "medusa-react"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import BackButton from "../../../components/atoms/back-button"
import Spinner from "../../../components/atoms/spinner"
import WidgetContainer from "../../../components/extensions/widget-container"
@@ -15,6 +16,7 @@ import { getErrorStatus } from "../../../utils/get-error-status"
const Manage = () => {
const navigate = useNavigate()
const { t } = useTranslation()
const { products, error } = useAdminProducts(
{
@@ -56,7 +58,7 @@ const Manage = () => {
<div className="pb-5xlarge">
<BackButton
path="/a/gift-cards"
label="Back to Gift Cards"
label={t("manage-back-to-gift-cards", "Back to Gift Cards")}
className="mb-xsmall"
/>
<div className="gap-x-base grid grid-cols-12">
@@ -5,6 +5,7 @@ import {
} from "medusa-react"
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import FileUploadField from "../../components/atoms/file-upload-field"
import Button from "../../components/fundamentals/button"
import PlusIcon from "../../components/fundamentals/icons/plus-icon"
@@ -43,6 +44,7 @@ const NewGiftCard = ({ onClose }: NewGiftCardProps) => {
const { mutate, isLoading } = useAdminCreateProduct()
const navigate = useNavigate()
const notification = useNotification()
const { t } = useTranslation()
const { register, setValue, handleSubmit, control } =
useForm<NewGiftCardFormData>({
@@ -75,13 +77,27 @@ const NewGiftCard = ({ onClose }: NewGiftCardProps) => {
const trimmedName = data.title.trim()
if (!trimmedName) {
notification("Error", "Please enter a name for the Gift Card", "error")
notification(
t("gift-cards-error", "Error"),
t(
"gift-cards-please-enter-a-name-for-the-gift-card",
"Please enter a name for the Gift Card"
),
"error"
)
focusByName("name")
return
}
if (!data.denominations?.length) {
notification("Error", "Please add at least one denomination", "error")
notification(
t("gift-cards-error", "Error"),
t(
"gift-cards-please-add-at-least-one-denomination",
"Please add at least one denomination"
),
"error"
)
focusByName("add-denomination")
return
}
@@ -105,7 +121,7 @@ const NewGiftCard = ({ onClose }: NewGiftCardProps) => {
title: data.title,
description: data.description,
discountable: false,
options: [{ title: "Denominations" }],
options: [{ title: t("gift-cards-denominations", "Denominations") }],
variants: data.denominations.map((d, i) => ({
title: `${i + 1}`,
inventory_quantity: 0,
@@ -121,12 +137,23 @@ const NewGiftCard = ({ onClose }: NewGiftCardProps) => {
},
{
onSuccess: () => {
notification("Success", "Successfully created Gift Card", "success")
notification(
t("gift-cards-success", "Success"),
t(
"gift-cards-successfully-created-gift-card",
"Successfully created Gift Card"
),
"success"
)
refetch()
navigate("/a/gift-cards/manage")
},
onError: (err) => {
notification("Error", getErrorMessage(err), "error")
notification(
t("gift-cards-error", "Error"),
getErrorMessage(err),
"error"
)
},
}
)
@@ -138,28 +165,40 @@ const NewGiftCard = ({ onClose }: NewGiftCardProps) => {
<Modal.Body>
<Modal.Header handleClose={onClose}>
<div>
<h1 className="inter-xlarge-semibold">Create Gift Card</h1>
<h1 className="inter-xlarge-semibold">
{t("gift-cards-create-gift-card", "Create Gift Card")}
</h1>
</div>
</Modal.Header>
<Modal.Content>
<div className="mb-base">
<h3 className="inter-base-semibold">Gift Card Details</h3>
<h3 className="inter-base-semibold">
{t("gift-cards-gift-card-details", "Gift Card Details")}
</h3>
</div>
<div className="gap-y-base flex flex-col">
<InputField
label={"Name"}
label={t("gift-cards-name", "Name")}
required
placeholder="The best Gift Card"
placeholder={t(
"gift-cards-the-best-gift-card",
"The best Gift Card"
)}
{...register("title", { required: true })}
/>
<TextArea
label="Description"
placeholder="The best Gift Card of all time"
label={t("gift-cards-description", "Description")}
placeholder={t(
"gift-cards-the-best-gift-card-of-all-time",
"The best Gift Card of all time"
)}
{...register("description")}
/>
</div>
<div className="mt-xlarge">
<h3 className="inter-base-semibold">Thumbnail</h3>
<h3 className="inter-base-semibold">
{t("gift-cards-thumbnail", "Thumbnail")}
</h3>
<div className="mt-base h-[80px]">
{thumbnail ? (
<div className="flex items-center gap-x-6">
@@ -178,7 +217,7 @@ const NewGiftCard = ({ onClose }: NewGiftCardProps) => {
type="button"
onClick={() => setValue("thumbnail", null)}
>
Delete
{t("gift-cards-delete", "Delete")}
</button>
</div>
</div>
@@ -192,14 +231,18 @@ const NewGiftCard = ({ onClose }: NewGiftCardProps) => {
"image/webp",
]}
onFileChosen={handleFileUpload}
placeholder="1200 x 1600 (3:4) recommended, up to 10MB each"
placeholder={t(
"gift-cards-size-recommended",
"1200 x 1600 (3:4) recommended, up to 10MB each"
)}
/>
)}
</div>
</div>
<div className="mt-xlarge">
<h3 className="inter-base-semibold mb-base">
Denominations<span className="text-rose-50">*</span>
{t("gift-cards-denominations", "Denominations")}
<span className="text-rose-50">*</span>
</h3>
<div className="gap-y-xsmall flex flex-col">
{fields.map((denomination, index) => {
@@ -223,7 +266,7 @@ const NewGiftCard = ({ onClose }: NewGiftCardProps) => {
render={({ field: { value, onChange, ref } }) => {
return (
<CurrencyInput.Amount
label="Amount"
label={t("gift-cards-amount", "Amount")}
amount={value || undefined}
onChange={onChange}
ref={ref}
@@ -257,7 +300,7 @@ const NewGiftCard = ({ onClose }: NewGiftCardProps) => {
type="button"
>
<PlusIcon size={20} />
Add Denomination
{t("gift-cards-add-denomination", "Add Denomination")}
</Button>
</div>
</Modal.Content>
@@ -270,7 +313,7 @@ const NewGiftCard = ({ onClose }: NewGiftCardProps) => {
className="w-eventButton"
onClick={onClose}
>
Cancel
{t("gift-cards-cancel", "Cancel")}
</Button>
<Button
type="submit"
@@ -280,7 +323,7 @@ const NewGiftCard = ({ onClose }: NewGiftCardProps) => {
loading={isLoading}
disabled={isLoading}
>
Create & Publish
{t("gift-cards-create-publish", "Create & Publish")}
</Button>
</div>
</Modal.Footer>
@@ -7,6 +7,7 @@ import {
} from "medusa-react"
import { useMemo, useState } from "react"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import PageDescription from "../../components/atoms/page-description"
import Spacer from "../../components/atoms/spacer"
import Spinner from "../../components/atoms/spinner"
@@ -26,6 +27,7 @@ import CustomGiftcard from "./custom-giftcard"
import NewGiftCard from "./new"
const Overview = () => {
const { t } = useTranslation()
const { products, isLoading } = useAdminProducts({
is_giftcard: true,
})
@@ -63,8 +65,20 @@ const Overview = () => {
{ status },
{
onSuccess: () =>
notification("Success", "Successfully updated Gift Card", "success"),
onError: (err) => notification("Error", getErrorMessage(err), "error"),
notification(
t("gift-cards-success", "Success"),
t(
"gift-cards-successfully-updated-gift-card",
"Successfully updated Gift Card"
),
"success"
),
onError: (err) =>
notification(
t("gift-cards-error", "Error"),
getErrorMessage(err),
"error"
),
}
)
}
@@ -79,7 +93,7 @@ const Overview = () => {
const actionables = [
{
label: "Custom Gift Card",
label: t("gift-cards-custom-gift-card", "Custom Gift Card"),
onClick: openCustom,
icon: <PlusIcon size={20} />,
},
@@ -102,8 +116,11 @@ const Overview = () => {
<>
<div className="flex flex-col">
<PageDescription
title="Gift Cards"
subtitle="Manage the Gift Cards of your Medusa store"
title={t("gift-cards-gift-cards", "Gift Cards")}
subtitle={t(
"gift-cards-manage",
"Manage the Gift Cards of your Medusa store"
)}
/>
{!isLoading ? (
<div className="gap-y-xsmall flex flex-col">
@@ -125,21 +142,32 @@ const Overview = () => {
onUnpublish={onUpdate}
/>
) : (
<BannerCard title="Are you ready to sell your first Gift Card?">
<BannerCard
title={t(
"gift-cards-are-you-ready-to-sell-your-first-gift-card",
"Are you ready to sell your first Gift Card?"
)}
>
<BannerCard.Description
cta={{
label: "Create Gift Card",
label: t("gift-cards-create-gift-card", "Create Gift Card"),
onClick: () => setShowCreate(true),
}}
>
No Gift Card has been added yet.
{t(
"gift-cards-no-gift-card-has-been-added-yet",
"No Gift Card has been added yet."
)}
</BannerCard.Description>
</BannerCard>
)}
<BodyCard
title="History"
subtitle="See the history of purchased Gift Cards"
title={t("gift-cards-history", "History")}
subtitle={t(
"gift-cards-see-the-history-of-purchased-gift-cards",
"See the history of purchased Gift Cards"
)}
actionables={actionables}
>
<GiftCardTable />
@@ -171,9 +199,12 @@ const Overview = () => {
<DeletePrompt
handleClose={closeDelete}
onDelete={async () => onDelete()}
successText="Successfully deleted Gift Card"
confirmText="Yes, delete"
heading="Delete Gift Card"
successText={t(
"gift-cards-successfully-deleted-gift-card",
"Successfully deleted Gift Card"
)}
confirmText={t("gift-cards-yes-delete", "Yes, delete")}
heading={t("gift-cards-delete-gift-card", "Delete Gift Card")}
/>
)}
<Spacer />
@@ -1,5 +1,6 @@
import clsx from "clsx"
import { useMemo, useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import PlusIcon from "../../components/fundamentals/icons/plus-icon"
import FilterDropdownContainer from "../../components/molecules/filter-dropdown/container"
import TabFilter from "../../components/molecules/filter-tab"
@@ -14,6 +15,7 @@ const ProductsFilter = ({
onRemoveTab,
onSaveTab,
}) => {
const { t } = useTranslation()
const [tempState, setTempState] = useState(filters)
const [name, setName] = useState("")
@@ -71,7 +73,7 @@ const ProductsFilter = ({
)}
>
<div className="rounded-rounded bg-grey-5 border-grey-20 inter-small-semibold flex h-6 items-center border px-2">
Filters
{t("inventory-filters", "Filters")}
<div className="text-grey-40 ml-1 flex items-center rounded">
<span className="text-violet-60 inter-small-semibold">
{numberOfFilters ? numberOfFilters : "0"}
@@ -3,6 +3,7 @@ import { StockLocationAddressDTO } from "@medusajs/types"
import { useAdminRegions } from "medusa-react"
import { useEffect, useMemo, useState } from "react"
import { Controller, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import InputField from "../../../../../components/molecules/input"
import { NextSelect } from "../../../../../components/molecules/select/next-select"
import FormValidator from "../../../../../utils/form-validator"
@@ -20,6 +21,7 @@ const AddressForm = ({
control,
} = form
const { t } = useTranslation()
const { regions } = useAdminRegions()
const watchedAddressForm = useWatch({
@@ -59,12 +61,14 @@ const AddressForm = ({
return (
<>
<span className="inter-base-semibold">Address</span>
<span className="inter-base-semibold">
{t("address-form-address", "Address")}
</span>
<div className="gap-y-large gap-x-large grid grid-cols-1">
<div className="gap-x-large grid grid-cols-2">
<InputField
label="Company"
placeholder="Company"
label={t("address-form-company", "Company")}
placeholder={t("address-form-company", "Company")}
errors={errors}
{...register(path("company"), {
pattern: FormValidator.whiteSpaceRule("Company"),
@@ -73,20 +77,23 @@ const AddressForm = ({
</div>
<div className="gap-x-large grid grid-cols-2">
<InputField
label="Address 1"
placeholder="Address 1"
label={t("address-form-address-1", "Address 1")}
placeholder={t("address-form-address-1", "Address 1")}
errors={errors}
required={addressFieldsRequired}
{...register(path("address_1"), {
pattern: FormValidator.whiteSpaceRule("Address 1"),
required: addressFieldsRequired
? "This field is required"
? t(
"address-form-this-field-is-required",
"This field is required"
)
: undefined,
})}
/>
<InputField
label="Address 2"
placeholder="Address 2"
label={t("address-form-address-2", "Address 2")}
placeholder={t("address-form-address-2", "Address 2")}
errors={errors}
{...register(path("address_2"), {
pattern: FormValidator.whiteSpaceRule("Address 2"),
@@ -95,16 +102,16 @@ const AddressForm = ({
</div>
<div className="gap-x-large grid grid-cols-2">
<InputField
label="Postal code"
placeholder="Postal code"
label={t("address-form-postal-code", "Postal code")}
placeholder={t("address-form-postal-code", "Postal code")}
errors={errors}
{...register(path("postal_code"), {
pattern: FormValidator.whiteSpaceRule("Postal code"),
})}
/>
<InputField
label="City"
placeholder="City"
label={t("address-form-city", "City")}
placeholder={t("address-form-city", "City")}
errors={errors}
{...register(path("city"), {
pattern: FormValidator.whiteSpaceRule("City"),
@@ -117,7 +124,10 @@ const AddressForm = ({
name={path("country_code")}
rules={{
required: addressFieldsRequired
? "This field is required"
? t(
"address-form-this-field-is-required",
"This field is required"
)
: undefined,
}}
render={({ field: { value, onChange } }) => {
@@ -134,7 +144,7 @@ const AddressForm = ({
return (
<NextSelect
label="Country"
label={t("address-form-country", "Country")}
required={addressFieldsRequired}
value={fieldValue}
options={countries}
@@ -2,8 +2,9 @@ import { SalesChannel } from "@medusajs/medusa"
import { StockLocationExpandedDTO } from "@medusajs/types"
import {
useAdminAddLocationToSalesChannel,
useAdminRemoveLocationFromSalesChannel
useAdminRemoveLocationFromSalesChannel,
} from "medusa-react"
import { useTranslation } from "react-i18next"
import SalesChannelsModal from "../../../../../components/forms/product/sales-channels-modal"
import Button from "../../../../../components/fundamentals/button"
import useToggleState from "../../../../../hooks/use-toggle-state"
@@ -18,6 +19,7 @@ const EditSalesChannels = ({
close: closeSalesChannelsModal,
open: openSalesChannelsModal,
} = useToggleState()
const { t } = useTranslation()
const { mutateAsync: addLocationToSalesChannel } =
useAdminAddLocationToSalesChannel()
@@ -62,7 +64,9 @@ const EditSalesChannels = ({
type="button"
onClick={openSalesChannelsModal}
>
{location.sales_channels?.length ? "Edit channels" : "Add channels"}
{location.sales_channels?.length
? t("edit-sales-channels-edit-channels", "Edit channels")
: t("edit-sales-channels-add-channels", "Add channels")}
</Button>
<SalesChannelsModal
open={showSalesChannelsModal}
@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next"
import FormValidator from "../../../../../utils/form-validator"
import InputField from "../../../../../components/molecules/input"
import { NestedForm } from "../../../../../utils/nested-form"
@@ -11,6 +12,7 @@ type Props = {
}
const GeneralForm = ({ form }: Props) => {
const { t } = useTranslation()
const {
register,
path,
@@ -20,11 +22,14 @@ const GeneralForm = ({ form }: Props) => {
<div>
<div className="gap-x-large mb-small grid grid-cols-2">
<InputField
label="Location name"
placeholder="Flagship store, warehouse"
label={t("general-form-location-name", "Location name")}
placeholder={t(
"general-form-flagship-store-warehouse",
"Flagship store, warehouse"
)}
required
{...register(path("name"), {
required: "Name is required",
required: t("general-form-name-is-required", "Name is required"),
pattern: FormValidator.whiteSpaceRule("Location name"),
})}
errors={errors}
@@ -1,12 +1,13 @@
import { StockLocationDTO } from "@medusajs/types"
import { useAdminDeleteStockLocation } from "medusa-react"
import React from "react"
import { useTranslation } from "react-i18next"
import IconBadge from "../../../../../components/fundamentals/icon-badge"
import BuildingsIcon from "../../../../../components/fundamentals/icons/buildings-icon"
import EditIcon from "../../../../../components/fundamentals/icons/edit-icon"
import TrashIcon from "../../../../../components/fundamentals/icons/trash-icon"
import Actionables, {
ActionType
ActionType,
} from "../../../../../components/molecules/actionables"
import useImperativeDialog from "../../../../../hooks/use-imperative-dialog"
import useNotification from "../../../../../hooks/use-notification"
@@ -22,6 +23,7 @@ type Props = {
}
const LocationCard: React.FC<Props> = ({ location }) => {
const { t } = useTranslation()
const { mutate: deleteLocation } = useAdminDeleteStockLocation(location.id)
const dialog = useImperativeDialog()
@@ -36,18 +38,32 @@ const LocationCard: React.FC<Props> = ({ location }) => {
const onDelete = async () => {
const shouldDelete = await dialog({
heading: "Delete Location",
text: "Are you sure you want to delete this location. This will also delete all inventory levels and reservations associated with this location.",
heading: t("location-card-delete-location", "Delete Location"),
text: t(
"location-card-confirm-delete",
"Are you sure you want to delete this location. This will also delete all inventory levels and reservations associated with this location."
),
extraConfirmation: true,
entityName: location.name,
})
if (shouldDelete) {
deleteLocation(undefined, {
onSuccess: () => {
notification("Success", "Location deleted successfully", "success")
notification(
t("location-card-success", "Success"),
t(
"location-card-location-deleted-successfully",
"Location deleted successfully"
),
"success"
)
},
onError: (err) => {
notification("Error", getErrorMessage(err), "error")
notification(
t("location-card-error", "Error"),
getErrorMessage(err),
"error"
)
},
})
}
@@ -55,13 +71,13 @@ const LocationCard: React.FC<Props> = ({ location }) => {
const DropdownActions: ActionType[] = [
{
label: "Edit details",
label: t("location-card-edit-details", "Edit details"),
onClick: openLocationEdit,
variant: "normal",
icon: <EditIcon size="20px" />,
},
{
label: "Delete",
label: t("location-card-delete", "Delete"),
onClick: onDelete,
variant: "danger",
icon: <TrashIcon size="20px" />,
@@ -90,7 +106,10 @@ const LocationCard: React.FC<Props> = ({ location }) => {
{isFeatureEnabled("sales_channels") && (
<div className="py-base border-grey-20 border-t border-solid px-6">
<h2 className="inter-small-semibold text-gray-500">
Connected sales channels
{t(
"location-card-connected-sales-channels",
"Connected sales channels"
)}
</h2>
<SalesChannelsSection location={location} />
</div>
@@ -1,5 +1,6 @@
import { SalesChannel, StockLocationExpandedDTO } from "@medusajs/medusa"
import { useFieldArray } from "react-hook-form"
import { useTranslation } from "react-i18next"
import SalesChannelsModal from "../../../../../components/forms/product/sales-channels-modal"
import Button from "../../../../../components/fundamentals/button"
import SalesChannelsList from "../../../../../components/molecules/sales-channels-list"
@@ -14,6 +15,7 @@ const SalesChannelsForm = ({
location: StockLocationExpandedDTO | null
form: NestedForm<AddSalesChannelsFormType>
}) => {
const { t } = useTranslation()
const {
state: showSalesChannelsModal,
close: closeSalesChannelsModal,
@@ -44,7 +46,7 @@ const SalesChannelsForm = ({
onClick={openSalesChannelsModal}
type="button"
>
Add sales channels
{t("sales-channels-form-add-sales-channels", "Add sales channels")}
</Button>
) : (
<div className="flex w-full items-center justify-between">
@@ -58,7 +60,7 @@ const SalesChannelsForm = ({
type="button"
onClick={openSalesChannelsModal}
>
Edit channels
{t("sales-channels-form-edit-channels", "Edit channels")}
</Button>
</div>
)}
@@ -1,4 +1,5 @@
import { StockLocationExpandedDTO } from "@medusajs/types"
import { useTranslation } from "react-i18next"
import SalesChannelsList from "../../../../../components/molecules/sales-channels-list"
import EditSalesChannels from "../edit-sales-channels"
@@ -7,12 +8,16 @@ const SalesChannelsSection = ({
}: {
location: StockLocationExpandedDTO
}) => {
const { t } = useTranslation()
return (
<div className="py-base">
<div className="flex w-full items-center justify-between">
{!location.sales_channels?.length ? (
<span className="inter-base-regular text-grey-40">
Not connected to any sales channels yet
{t(
"sales-channels-section-not-connected-to-any-sales-channels-yet",
"Not connected to any sales channels yet"
)}
</span>
) : (
<SalesChannelsList salesChannels={location.sales_channels} />
@@ -4,6 +4,8 @@ import {
StockLocationAddressInput,
StockLocationDTO,
} from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import GeneralForm, { GeneralFormType } from "../components/general-form"
import MetadataForm, {
MetadataFormType,
@@ -32,6 +34,7 @@ export type LocationEditModalProps = {
}
const LocationEditModal = ({ onClose, location }: LocationEditModalProps) => {
const { t } = useTranslation()
const form = useForm<EditLocationForm>({
defaultValues: {
general: {
@@ -56,10 +59,17 @@ const LocationEditModal = ({ onClose, location }: LocationEditModalProps) => {
mutate(payload, {
onSuccess: () => {
onClose()
notification("Success", "Location edited successfully", "success")
notification(
t("edit-success", "Success"),
t(
"edit-location-edited-successfully",
"Location edited successfully"
),
"success"
)
},
onError: (err) => {
notification("Error", getErrorMessage(err), "error")
notification(t("edit-error", "Error"), getErrorMessage(err), "error")
},
})
})
@@ -68,7 +78,9 @@ const LocationEditModal = ({ onClose, location }: LocationEditModalProps) => {
<Modal handleClose={onClose}>
<Modal.Body className="top-20">
<Modal.Header handleClose={onClose}>
<h1 className="text-xl font-semibold">Edit Location Details</h1>
<h1 className="text-xl font-semibold">
{t("edit-edit-location-details", "Edit Location Details")}
</h1>
</Modal.Header>
<Modal.Content>
<form className="w-full">
@@ -76,7 +88,9 @@ const LocationEditModal = ({ onClose, location }: LocationEditModalProps) => {
<GeneralForm form={nestedForm(form, "general")} />
<AddressForm form={nestedForm(form, "address")} />
<div>
<h2 className="inter-base-semibold mb-base">Metadata</h2>
<h2 className="inter-base-semibold mb-base">
{t("edit-metadata", "Metadata")}
</h2>
<MetadataForm form={nestedForm(form, "metadata")} />
</div>
</div>
@@ -91,7 +105,7 @@ const LocationEditModal = ({ onClose, location }: LocationEditModalProps) => {
type="button"
onClick={onClose}
>
Cancel
{t("edit-cancel", "Cancel")}
</Button>
<Button
size="small"
@@ -100,7 +114,7 @@ const LocationEditModal = ({ onClose, location }: LocationEditModalProps) => {
disabled={!isDirty || isLoading}
onClick={onSubmit}
>
Save and close
{t("edit-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>
@@ -1,4 +1,5 @@
import { AdminPostStockLocationsReq, SalesChannel } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import GeneralForm, { GeneralFormType } from "../components/general-form"
import MetadataForm, {
MetadataFormType,
@@ -38,6 +39,7 @@ type NewLocationForm = {
}
const NewLocation = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation()
const [accordionValue, setAccordionValue] = React.useState("general")
const form = useForm<NewLocationForm>({
defaultValues: {
@@ -111,12 +113,19 @@ const NewLocation = ({ onClose }: { onClose: () => void }) => {
)
)
.then(() => {
notification("Success", "Location added successfully", "success")
notification(
t("new-success", "Success"),
t("new-location-added-successfully", "Location added successfully"),
"success"
)
})
.catch(() => {
notification(
"Error",
"Location was created successfully, but there was an error associating sales channels",
t("new-error", "Error"),
t(
"new-location-created",
"Location was created successfully, but there was an error associating sales channels"
),
"error"
)
})
@@ -124,7 +133,7 @@ const NewLocation = ({ onClose }: { onClose: () => void }) => {
onClose()
})
} catch (err) {
notification("Error", getErrorMessage(err), "error")
notification(t("new-error", "Error"), getErrorMessage(err), "error")
}
}
@@ -143,9 +152,15 @@ const NewLocation = ({ onClose }: { onClose: () => void }) => {
</Button>
{isShowingClosePrompt && (
<DeletePrompt
heading="Are you sure you want to cancel with unsaved changes"
confirmText="Yes, cancel"
cancelText="No, continue creating"
heading={t(
"new-cancel-location-changes",
"Are you sure you want to cancel with unsaved changes"
)}
confirmText={t("new-yes-cancel", "Yes, cancel")}
cancelText={t(
"new-no-continue-creating",
"No, continue creating"
)}
successText={false}
handleClose={closeClosePrompt}
onDelete={async () => onClose()}
@@ -158,7 +173,7 @@ const NewLocation = ({ onClose }: { onClose: () => void }) => {
type="submit"
disabled={!isDirty}
>
Add location
{t("new-add-location", "Add location")}
</Button>
</div>
</div>
@@ -166,7 +181,7 @@ const NewLocation = ({ onClose }: { onClose: () => void }) => {
<FocusModal.Main className="no-scrollbar flex w-full justify-center">
<div className="medium:w-7/12 large:w-6/12 small:w-4/5 my-16 max-w-[700px]">
<h1 className="mb-base text-grey-90 text-xlarge px-1 font-semibold">
Add new location
{t("new-add-new-location", "Add new location")}
</h1>
<Accordion
value={accordionValue}
@@ -175,17 +190,22 @@ const NewLocation = ({ onClose }: { onClose: () => void }) => {
>
<Accordion.Item
value={"general"}
title={"General Information"}
title={t("new-general-information", "General Information")}
required
>
<p className="inter-base-regular text-grey-50">
Specify the details about this location
{t(
"new-location-details",
"Specify the details about this location"
)}
</p>
<div className="mt-xlarge gap-y-xlarge flex flex-col pb-0.5">
<GeneralForm form={nestedForm(form, "general")} />
<AddressForm form={nestedForm(form, "address")} />
<div>
<h2 className="inter-base-semibold mb-base">Metadata</h2>
<h2 className="inter-base-semibold mb-base">
{t("new-metadata", "Metadata")}
</h2>
<MetadataForm form={nestedForm(form, "metadata")} />
</div>
</div>
@@ -196,8 +216,10 @@ const NewLocation = ({ onClose }: { onClose: () => void }) => {
title={"Sales Channels"}
>
<p className="inter-base-regular text-grey-50">
Specify which Sales Channels this location's items can be
purchased through.
{t(
"new-select-location-channel",
"Specify which Sales Channels this location's items can be purchased through."
)}
</p>
<div className="mt-xlarge flex">
<SalesChannelsForm
@@ -1,8 +1,10 @@
import { useParams } from "react-router-dom"
import { useTranslation } from "react-i18next"
import Button from "../../components/fundamentals/button"
import Medusa from "../../services/api"
const Oauth = () => {
const { t } = useTranslation()
const { app_name, code, state } = useParams()
return (
<>
@@ -18,7 +20,7 @@ const Oauth = () => {
})
}
>
Complete Installation
{t("oauth-complete-installation", "Complete Installation")}
</Button>
</>
)
@@ -1,4 +1,5 @@
import { Controller } from "react-hook-form"
import { useTranslation } from "react-i18next"
import RadioGroup from "../../../../components/organisms/radio-group"
import { NestedForm } from "../../../../utils/nested-form"
@@ -11,6 +12,7 @@ type Props = {
}
const ClaimTypeForm = ({ form }: Props) => {
const { t } = useTranslation()
const { control, path } = form
return (
@@ -26,12 +28,12 @@ const ClaimTypeForm = ({ form }: Props) => {
className="flex items-center"
>
<RadioGroup.SimpleItem
label="Refund"
label={t("claim-type-form-refund", "Refund")}
value="refund"
onChange={onChange}
/>
<RadioGroup.SimpleItem
label="Replace"
label={t("claim-type-form-replace", "Replace")}
value="replace"
onChange={onChange}
/>
@@ -1,6 +1,7 @@
import { Order } from "@medusajs/medusa"
import { getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { FieldArrayWithId, useFieldArray } from "react-hook-form"
import { useTranslation } from "react-i18next"
import InputError from "../../../../components/atoms/input-error"
import { NestedForm } from "../../../../utils/nested-form"
import { ItemsToReceiveTable } from "./items-to-receive-table"
@@ -43,6 +44,7 @@ export const ItemsToReceiveForm = ({ form, order }: Props) => {
formState: { errors },
} = form
const { t } = useTranslation()
const { fields } = useFieldArray({
control,
name: path("items"),
@@ -62,7 +64,9 @@ export const ItemsToReceiveForm = ({ form, order }: Props) => {
return (
<div className="gap-y-xsmall flex flex-col">
<h2 className="inter-base-semibold">Items to receive</h2>
<h2 className="inter-base-semibold">
{t("items-to-receive-form-items-to-receive", "Items to receive")}
</h2>
<ItemsToReceiveTable instance={tableInstance} />
<InputError errors={errors} name={path("items")} />
@@ -1,6 +1,7 @@
import { createColumnHelper } from "@tanstack/react-table"
import React, { ChangeEvent, useCallback, useMemo } from "react"
import { Controller, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import CopyToClipboard from "../../../../components/atoms/copy-to-clipboard"
import Thumbnail from "../../../../components/atoms/thumbnail"
import IndeterminateCheckbox from "../../../../components/molecules/indeterminate-checkbox"
@@ -20,6 +21,7 @@ type Props = {
}
export const useItemsToReceiveColumns = ({ form, orderCurrency }: Props) => {
const { t } = useTranslation()
const { control, setValue, getValues, path } = form
const updateQuantity = useCallback(
@@ -90,7 +92,7 @@ export const useItemsToReceiveColumns = ({ form, orderCurrency }: Props) => {
},
}),
columnHelper.accessor("variant_title", {
header: "Product",
header: t("items-to-receive-form-product", "Product"),
cell: ({ getValue, row: { original } }) => {
const value = getValue()
@@ -120,7 +122,11 @@ export const useItemsToReceiveColumns = ({ form, orderCurrency }: Props) => {
}),
columnHelper.display({
id: "quantity",
header: () => <p className="text-right">Quantity</p>,
header: () => (
<p className="text-right">
{t("items-to-receive-form-quantity", "Quantity")}
</p>
),
maxSize: 50,
cell: ({
row: {
@@ -147,7 +153,11 @@ export const useItemsToReceiveColumns = ({ form, orderCurrency }: Props) => {
}),
columnHelper.accessor("refundable", {
maxSize: 80,
header: () => <p className="text-right">Refundable</p>,
header: () => (
<p className="text-right">
{t("items-to-receive-form-refundable", "Refundable")}
</p>
),
cell: ({ getValue }) => {
return (
<p className="text-right">
@@ -1,6 +1,7 @@
import { useAdminReturnReasons } from "medusa-react"
import { useMemo } from "react"
import { Controller, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { ReturnReasonDetails } from ".."
import Button from "../../../../../components/fundamentals/button"
import Modal from "../../../../../components/molecules/modal"
@@ -40,6 +41,7 @@ const AddReasonScreen = ({
isClaim = false,
addReasonDetails,
}: Props) => {
const { t } = useTranslation()
const { return_reasons } = useAdminReturnReasons()
const returnReasonOptions = useMemo(() => {
if (isClaim) {
@@ -74,15 +76,20 @@ const AddReasonScreen = ({
<>
<Modal.Content>
<div className="gap-y-base flex flex-col">
<h2 className="inter-base-semibold">Reason for Return</h2>
<h2 className="inter-base-semibold">
{t("add-return-reason-reason-for-return", "Reason for Return")}
</h2>
<Controller
control={control}
name="reason"
render={({ field }) => {
return (
<NextSelect
label="Reason"
placeholder="Choose a return reason"
label={t("add-return-reason-reason", "Reason")}
placeholder={t(
"add-return-reason-choose-a-return-reason",
"Choose a return reason"
)}
{...field}
options={returnReasonOptions}
isClearable
@@ -91,8 +98,11 @@ const AddReasonScreen = ({
}}
/>
<TextArea
label="Note"
placeholder="Product was damaged during shipping"
label={t("add-return-reason-note", "Note")}
placeholder={t(
"add-return-reason-product-was-damaged-during-shipping",
"Product was damaged during shipping"
)}
{...register("note")}
/>
</div>
@@ -100,7 +110,7 @@ const AddReasonScreen = ({
<Modal.Footer>
<div className="gap-x-xsmall flex w-full items-center justify-end">
<Button size="small" variant="secondary" onClick={pop} type="button">
Cancel
{t("add-return-reason-cancel", "Cancel")}
</Button>
<Button
size="small"
@@ -109,7 +119,7 @@ const AddReasonScreen = ({
disabled={!isDirty}
type="button"
>
Save and go back
{t("add-return-reason-save-and-go-back", "Save and go back")}
</Button>
</div>
</Modal.Footer>
@@ -119,10 +129,11 @@ const AddReasonScreen = ({
export const useAddReasonScreen = () => {
const { pop, push } = useLayeredModal()
const { t } = useTranslation()
const pushScreen = (props: Props) => {
push({
title: "Select Reason",
title: t("add-return-reason-select-reason-title", "Select Reason"),
onBack: () => pop(),
view: <AddReasonScreen {...props} />,
})
@@ -1,5 +1,6 @@
import { Row } from "@tanstack/react-table"
import { Controller, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import {
ItemsToReturnFormType,
ReturnItemObject,
@@ -25,6 +26,7 @@ const AddReturnReason = ({ row, form, isClaim = false }: Props) => {
formState: { errors },
} = form
const { t } = useTranslation()
const { pushScreen } = useAddReasonScreen()
const reasonDetails = useWatch({
@@ -69,7 +71,9 @@ const AddReturnReason = ({ row, form, isClaim = false }: Props) => {
}
>
<span>
{reasonDetails?.reason ? "Edit" : "Select"} reason
{reasonDetails?.reason
? t("add-return-reason-edit-reason", "Edit reason")
: t("add-return-reason-select-reason", "Select reason")}
</span>
</Button>
)
@@ -5,6 +5,7 @@ import {
useReactTable,
} from "@tanstack/react-table"
import { FieldArrayWithId, useFieldArray } from "react-hook-form"
import { useTranslation } from "react-i18next"
import InputError from "../../../../components/atoms/input-error"
import { FormImage, Option } from "../../../../types/shared"
import { NestedForm } from "../../../../utils/nested-form"
@@ -50,6 +51,7 @@ type Props = {
}
const ItemsToReturnForm = ({ form, order, isClaim = false }: Props) => {
const { t } = useTranslation()
const {
control,
path,
@@ -78,7 +80,9 @@ const ItemsToReturnForm = ({ form, order, isClaim = false }: Props) => {
return (
<div className="gap-y-base flex flex-col">
<h2 className="inter-base-semibold">
Items to {isClaim ? "claim" : "return"}
{isClaim
? t("items-to-return-form-items-to-claim", "Items to claim")
: t("items-to-return-form-items-to-return", "Items to return")}
</h2>
<ItemsToReturnTable form={form} instance={table} isClaim={isClaim} />
<InputError errors={errors} name={path("items")} />
@@ -1,6 +1,7 @@
import { createColumnHelper } from "@tanstack/react-table"
import React, { useCallback, useMemo } from "react"
import { Controller, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { ItemsToReturnFormType, ReturnItemObject } from "."
import CopyToClipboard from "../../../../components/atoms/copy-to-clipboard"
import { Thumbnail } from "../../../../components/atoms/thumbnail/thumbnail"
@@ -17,6 +18,7 @@ type Props = {
}
export const useItemsToReturnColumns = ({ form, orderCurrency }: Props) => {
const { t } = useTranslation()
const { control, setValue, getValues, path } = form
const updateQuantity = useCallback(
@@ -97,7 +99,7 @@ export const useItemsToReturnColumns = ({ form, orderCurrency }: Props) => {
},
}),
columnHelper.accessor("variant_title", {
header: "Product",
header: t("items-to-return-form-product", "Product"),
cell: ({ getValue, row: { original } }) => {
const value = getValue()
@@ -127,7 +129,11 @@ export const useItemsToReturnColumns = ({ form, orderCurrency }: Props) => {
}),
columnHelper.display({
id: "quantity",
header: () => <p className="text-right">Quantity</p>,
header: () => (
<p className="text-right">
{t("items-to-return-form-quantity", "Quantity")}
</p>
),
maxSize: 50,
cell: ({
row: {
@@ -154,7 +160,11 @@ export const useItemsToReturnColumns = ({ form, orderCurrency }: Props) => {
}),
columnHelper.accessor("refundable", {
maxSize: 80,
header: () => <p className="text-right">Refundable</p>,
header: () => (
<p className="text-right">
{t("items-to-return-form-refundable", "Refundable")}
</p>
),
cell: ({ getValue }) => {
return (
<p className="text-right">
@@ -11,6 +11,7 @@ import {
} from "@tanstack/react-table"
import { useAdminVariants } from "medusa-react"
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../../../../components/fundamentals/button"
import Modal from "../../../../../components/molecules/modal"
import { useLayeredModal } from "../../../../../components/molecules/modal/layered-modal"
@@ -36,6 +37,7 @@ const AddAdditionalItemsScreen = ({
order,
}: Props) => {
const { pop } = useLayeredModal()
const { t } = useTranslation()
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -162,7 +164,7 @@ const AddAdditionalItemsScreen = ({
<Modal.Footer>
<div className="gap-x-xsmall flex w-full items-center justify-end">
<Button variant="secondary" size="small" onClick={pop}>
Go back
{t("add-additional-items-screen-go-back", "Go back")}
</Button>
<Button
variant="primary"
@@ -170,7 +172,7 @@ const AddAdditionalItemsScreen = ({
type="button"
onClick={onSubmit}
>
Add products
{t("add-additional-items-screen-add-products", "Add products")}
</Button>
</div>
</Modal.Footer>
@@ -180,10 +182,14 @@ const AddAdditionalItemsScreen = ({
export const useAddAdditionalItemsScreen = () => {
const { push, pop } = useLayeredModal()
const { t } = useTranslation()
const pushScreen = (props: Props) => {
push({
title: "Add Product Variants",
title: t(
"add-additional-items-screen-add-product-variants",
"Add Product Variants"
),
onBack: () => pop(),
view: <AddAdditionalItemsScreen {...props} />,
})
@@ -1,6 +1,7 @@
import { PricedVariant } from "@medusajs/medusa/dist/types/pricing"
import { flexRender, Table as Instance } from "@tanstack/react-table"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import LoadingContainer from "../../../../../components/atoms/loading-container"
import Table from "../../../../../components/molecules/table"
@@ -17,6 +18,7 @@ export const AddAdditionalItemsTable = ({
}: Props) => {
const [query, setQuery] = useState<string | undefined>(undefined)
const { getHeaderGroups, getRowModel } = instance
const { t } = useTranslation()
useEffect(() => {
setSearchTerm(query)
@@ -26,7 +28,10 @@ export const AddAdditionalItemsTable = ({
<LoadingContainer isLoading={isLoadingData}>
<Table
enableSearch
searchPlaceholder="Search products"
searchPlaceholder={t(
"add-additional-items-screen-search-products",
"Search products"
)}
searchValue={query}
handleSearch={setQuery}
>
@@ -3,6 +3,7 @@ import { PricedVariant } from "@medusajs/medusa/dist/types/pricing"
import { createColumnHelper } from "@tanstack/react-table"
import clsx from "clsx"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import Thumbnail from "../../../../../components/atoms/thumbnail"
import Tooltip from "../../../../../components/atoms/tooltip"
import SortingIcon from "../../../../../components/fundamentals/icons/sorting-icon"
@@ -12,6 +13,7 @@ import { formatAmountWithSymbol } from "../../../../../utils/prices"
const columnHelper = createColumnHelper<PricedVariant>()
export const useAddAdditionalItemsColumns = (order: Order) => {
const { t } = useTranslation()
const columns = useMemo(
() => [
columnHelper.display({
@@ -49,9 +51,10 @@ export const useAddAdditionalItemsColumns = (order: Order) => {
/>
) : (
<Tooltip
content={
content={t(
"add-additional-items-screen-variant-price-missing",
"This variant does not have a price for the region/currency of this order, and cannot be selected."
}
)}
>
<IndeterminateCheckbox
checked={getIsSelected()}
@@ -124,7 +127,11 @@ export const useAddAdditionalItemsColumns = (order: Order) => {
}),
columnHelper.accessor("inventory_quantity", {
maxSize: 20,
header: () => <p className="select-none text-right">Stock</p>,
header: () => (
<p className="select-none text-right">
{t("add-additional-items-screen-stock", "Stock")}
</p>
),
cell: ({ cell: { getValue }, row: { getCanSelect } }) => {
const isSelectable = getCanSelect()
@@ -141,7 +148,11 @@ export const useAddAdditionalItemsColumns = (order: Order) => {
}),
columnHelper.accessor("calculated_price_incl_tax", {
maxSize: 80,
header: () => <p className="text-right">Price</p>,
header: () => (
<p className="text-right">
{t("add-additional-items-screen-price", "Price")}
</p>
),
cell: ({
getValue,
row: {
@@ -154,7 +165,10 @@ export const useAddAdditionalItemsColumns = (order: Order) => {
<div className="text-right">
{original_price_incl_tax !== price && (
<Tooltip
content="The price has been overridden in a price list, that is applicable to this order."
content={t(
"add-additional-items-screen-price-overridden-in-price-list-applicable-to-this-order",
"The price has been overridden in a price list, that is applicable to this order."
)}
side="top"
>
<p className="text-grey-40 cursor-default line-through">
@@ -2,6 +2,7 @@ import { Order } from "@medusajs/medusa"
import { getCoreRowModel, useReactTable } from "@tanstack/react-table"
import clsx from "clsx"
import { FieldArrayWithId, useFieldArray } from "react-hook-form"
import { useTranslation } from "react-i18next"
import Button from "../../../../components/fundamentals/button"
import { NestedForm } from "../../../../utils/nested-form"
import { useAddAdditionalItemsScreen } from "./add-additional-items-screen"
@@ -38,6 +39,7 @@ type Props = {
}
const ItemsToSendForm = ({ form, order }: Props) => {
const { t } = useTranslation()
const { control, path } = form
const { fields, append, remove } = useFieldArray({
@@ -68,7 +70,9 @@ const ItemsToSendForm = ({ form, order }: Props) => {
})}
>
<div className="flex w-full items-center">
<h2 className="inter-base-semibold">Items to send</h2>
<h2 className="inter-base-semibold">
{t("items-to-send-form-items-to-send", "Items to send")}
</h2>
</div>
{fields.length > 0 && <AdditionalItemsTable instance={tableInstance} />}
@@ -86,7 +90,7 @@ const ItemsToSendForm = ({ form, order }: Props) => {
})
}
>
Add products
{t("items-to-send-form-add-products", "Add products")}
</Button>
</div>
</div>
@@ -1,5 +1,6 @@
import { createColumnHelper } from "@tanstack/react-table"
import { useCallback, useMemo } from "react"
import { useTranslation } from "react-i18next"
import CopyToClipboard from "../../../../components/atoms/copy-to-clipboard"
import Thumbnail from "../../../../components/atoms/thumbnail"
import Tooltip from "../../../../components/atoms/tooltip"
@@ -23,6 +24,7 @@ export const useAdditionalItemsColumns = ({
orderCurrency,
removeItem,
}: AdditionalItemsColumnProps) => {
const { t } = useTranslation()
const { control, setValue, getValues, path } = form
const updateQuantity = useCallback(
@@ -39,7 +41,7 @@ export const useAdditionalItemsColumns = ({
return [
columnHelper.display({
id: "product_display",
header: "Product",
header: t("items-to-send-form-product", "Product"),
cell: ({
row: {
original: { thumbnail, product_title, variant_title, sku },
@@ -71,7 +73,11 @@ export const useAdditionalItemsColumns = ({
}),
columnHelper.display({
id: "quantity",
header: () => <p className="text-right">Quantity</p>,
header: () => (
<p className="text-right">
{t("items-to-send-form-quantity", "Quantity")}
</p>
),
maxSize: 50,
cell: ({
row: {
@@ -96,7 +102,9 @@ export const useAdditionalItemsColumns = ({
}),
columnHelper.accessor("price", {
maxSize: 50,
header: () => <p className="text-right">Price</p>,
header: () => (
<p className="text-right">{t("items-to-send-form-price", "Price")}</p>
),
cell: ({
getValue,
row: {
@@ -109,7 +117,10 @@ export const useAdditionalItemsColumns = ({
<div className="text-right">
{original_price !== price && (
<Tooltip
content="The price has been overridden in a price list, that is applicable to this order."
content={t(
"items-to-send-form-price-overridden-in-price-list-applicable-to-this-order",
"The price has been overridden in a price list, that is applicable to this order."
)}
side="top"
>
<p className="text-grey-40 cursor-default line-through">
@@ -1,5 +1,6 @@
import { Order } from "@medusajs/medusa"
import { Controller, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import Button from "../../../../components/fundamentals/button"
import CrossIcon from "../../../../components/fundamentals/icons/cross-icon"
import EditIcon from "../../../../components/fundamentals/icons/edit-icon"
@@ -18,6 +19,7 @@ export type RefundAmountFormType = {
}
const RefundAmountForm = ({ form, initialValue = 0, order }: Props) => {
const { t } = useTranslation()
const {
control,
path,
@@ -49,7 +51,10 @@ const RefundAmountForm = ({ form, initialValue = 0, order }: Props) => {
size="small"
type="button"
className="h-10 w-10"
aria-label="Cancel editing refund amount"
aria-label={t(
"refund-amount-form-cancel-editing-refund-amount",
"Cancel editing refund amount"
)}
onClick={disableEdit}
>
<CrossIcon size={16} className="text-grey-40" />
@@ -60,7 +65,10 @@ const RefundAmountForm = ({ form, initialValue = 0, order }: Props) => {
size="small"
type="button"
onClick={enableEdit}
aria-label="Edit refund amount"
aria-label={t(
"refund-amount-form-edit-refund-amount",
"Edit refund amount"
)}
className="h-10 w-10"
>
<EditIcon size={16} className="text-grey-40" />
@@ -75,12 +83,18 @@ const RefundAmountForm = ({ form, initialValue = 0, order }: Props) => {
rules={{
min: {
value: 0,
message: "Refund amount cannot be negative",
message: t(
"refund-amount-form-refund-amount-cannot-be-negative",
"Refund amount cannot be negative"
),
},
required: true,
validate: (value) => {
if (value === undefined || !(value >= 0)) {
return "The refund amount must be at least 0"
return t(
"refund-amount-form-the-refund-amount-must-be-at-least-0",
"The refund amount must be at least 0"
)
}
},
}}
@@ -10,6 +10,7 @@ import { ReservationItemDTO } from "@medusajs/types"
import Tooltip from "../../../../components/atoms/tooltip"
import WarningCircleIcon from "../../../../components/fundamentals/icons/warning-circle"
import { sum } from "lodash"
import { useTranslation } from "react-i18next"
const ReservationIndicator = ({
reservations,
@@ -18,6 +19,7 @@ const ReservationIndicator = ({
reservations?: ReservationItemDTO[]
lineItem: LineItem
}) => {
const { t } = useTranslation()
const { variant, isLoading, isFetching } = useAdminVariantsInventory(
lineItem.variant_id!,
{
@@ -58,7 +60,13 @@ const ReservationIndicator = ({
<div className="gap-y-base grid grid-cols-1 divide-y">
{!!awaitingReservation && (
<span className="flex w-full items-center">
{awaitingReservation} items not reserved
{t(
"reservation-indicator-awaiting-reservation-count",
"{{awaitingReservation}} items not reserved",
{
awaitingReservation,
}
)}
</span>
)}
{reservations?.map((reservation) => (
@@ -74,7 +82,10 @@ const ReservationIndicator = ({
</div>
) : (
<span className="flex w-full items-center">
This item has been fulfilled.
{t(
"reservation-indicator-this-item-has-been-fulfilled",
"This item has been fulfilled."
)}
</span>
)}
</div>
@@ -115,16 +126,24 @@ const EditReservationButton = ({
lineItem: LineItem
onClick: () => void
}) => {
const { t } = useTranslation()
return (
<div className="pt-base first:pt-0">
{`${reservation.quantity} item: ${locationName}`}
{t(
"edit-reservation-button-quantity-item-location-name",
"{{quantity}} item: ${{locationName}}",
{
quantity: reservation.quantity,
locationName,
}
)}
<Button
onClick={onClick}
variant="ghost"
size="small"
className="mt-2 w-full border"
>
Edit reservation
{t("reservation-indicator-edit-reservation", "Edit reservation")}
</Button>
</div>
)
@@ -1,6 +1,7 @@
import { Order } from "@medusajs/medusa"
import { useMemo } from "react"
import { UseFormReturn, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import IconTooltip from "../../../../components/molecules/icon-tooltip"
import { nestedForm } from "../../../../utils/nested-form"
import { formatAmountWithSymbol } from "../../../../utils/prices"
@@ -15,6 +16,7 @@ type Props = {
}
export const ClaimSummary = ({ form, order }: Props) => {
const { t } = useTranslation()
const { control } = form
const claimItems = useWatch({
@@ -77,7 +79,9 @@ export const ClaimSummary = ({ form, order }: Props) => {
<div className="gap-y-base border-grey-20 py-large flex flex-col border-y">
{selectedClaimItems.length > 0 && (
<div>
<p className="inter-base-semibold mb-small">Claimed items</p>
<p className="inter-base-semibold mb-small">
{t("rma-summaries-claimed-items", "Claimed items")}
</p>
<div className="gap-y-xsmall flex flex-col">
{selectedClaimItems.map((item, index) => {
return (
@@ -110,12 +114,15 @@ export const ClaimSummary = ({ form, order }: Props) => {
{claimType !== "refund" && replacementItems.length > 0 && (
<div>
<div className="mb-small gap-x-2xsmall flex items-center">
<p className="inter-base-semibold">Replacement items</p>
<p className="inter-base-semibold">
{t("rma-summaries-replacement-items", "Replacement items")}
</p>
<IconTooltip
type="warning"
content={
content={t(
"rma-summaries-customer-refund-description",
"The customer will receive a full refund for the claimed items, as the cost of replacement items and shipping will not be deducted. Alternatively, you can choose to set a custom refund amount when you receive the returned items or create an exchange instead."
}
)}
/>
</div>
<div className="gap-y-xsmall flex flex-col">
@@ -153,13 +160,21 @@ export const ClaimSummary = ({ form, order }: Props) => {
data-testid="refund-amount-container"
>
<div className="gap-x-2xsmall flex items-center">
<p className="inter-base-semibold">Refund amount</p>
<p className="inter-base-semibold">
{t("rma-summaries-refund-amount", "Refund amount")}
</p>
<IconTooltip
type="info"
content={
claimType === "replace" && claimItemShipping.option
? "The customer will be refunded once the returned items are received"
: "The customer will be refunded immediately"
? t(
"rma-summaries-the-customer-will-be-refunded-once-the-returned-items-are-received",
"The customer will be refunded once the returned items are received"
)
: t(
"rma-summaries-the-customer-will-be-refunded-immediately",
"The customer will be refunded immediately"
)
}
/>
</div>
@@ -1,6 +1,7 @@
import { Order, Return } from "@medusajs/medusa"
import { useMemo } from "react"
import { UseFormReturn, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { nestedForm } from "../../../../utils/nested-form"
import { ReceiveReturnFormType } from "../../details/receive-return"
import RefundAmountForm from "../refund-amount-form"
@@ -15,6 +16,7 @@ type Props = {
export const ReceiveReturnSummary = ({ form, order, returnRequest }: Props) => {
const { control } = form
const { t } = useTranslation()
const items = useWatch({
control,
@@ -74,7 +76,9 @@ export const ReceiveReturnSummary = ({ form, order, returnRequest }: Props) => {
<div className="gap-y-base border-grey-20 py-large flex flex-col border-y">
{itemToReceive.length > 0 && (
<div>
<p className="inter-base-semibold mb-small">Receiving</p>
<p className="inter-base-semibold mb-small">
{t("rma-summaries-receiving", "Receiving")}
</p>
<div className="gap-y-xsmall flex flex-col">
{itemToReceive.map((item, index) => {
return (
@@ -107,7 +111,9 @@ export const ReceiveReturnSummary = ({ form, order, returnRequest }: Props) => {
className="inter-large-semibold flex items-center justify-between"
data-testid="refund-amount-container"
>
<p className="inter-base-semibold">Refund amount</p>
<p className="inter-base-semibold">
{t("rma-summaries-refund-amount", "Refund amount")}
</p>
<RefundAmountForm
form={nestedForm(form, "refund_amount")}
order={order}
@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next"
import CornerDownRightIcon from "../../../../components/fundamentals/icons/corner-down-right-icon"
import { formatAmountWithSymbol } from "../../../../utils/prices"
@@ -14,6 +15,7 @@ export const SummaryShippingLine = ({
price,
currencyCode,
}: ShippingLineProps) => {
const { t } = useTranslation()
return (
<div className="flex items-center justify-between">
<div className="gap-x-base flex items-center">
@@ -34,7 +36,7 @@ export const SummaryShippingLine = ({
amount: price,
currency: currencyCode,
})
: "Free"}
: t("rma-summaries-free", "Free")}
</p>
</div>
)
@@ -1,4 +1,5 @@
import { Controller } from "react-hook-form"
import { useTranslation } from "react-i18next"
import IconTooltip from "../../../../components/molecules/icon-tooltip"
import IndeterminateCheckbox from "../../../../components/molecules/indeterminate-checkbox"
import { NestedForm } from "../../../../utils/nested-form"
@@ -13,12 +14,13 @@ type Props = {
}
const SendNotificationForm = ({ form, type }: Props) => {
const { t } = useTranslation()
const { control, path } = form
const subject = {
return: "return",
swap: "exchange",
claim: "claim",
return: t("send-notification-form-return", "return"),
swap: t("send-notification-form-exchange", "exchange"),
claim: t("send-notification-form-claim", "claim"),
}[type]
return (
@@ -31,10 +33,19 @@ const SendNotificationForm = ({ form, type }: Props) => {
<div className="mr-xsmall">
<IndeterminateCheckbox checked={value} onChange={onChange} />
</div>
<p className="inter-small-semibold mr-1.5">Send notifications</p>
<p className="inter-small-semibold mr-1.5">
{t(
"send-notification-form-send-notifications",
"Send notifications"
)}
</p>
<IconTooltip
type="info"
content={`If unchecked the customer will not receive communication about this ${subject}.`}
content={t(
"send-notification-form-if-unchecked-the-customer-will-not-receive-communication",
"If unchecked the customer will not receive communication about this {{subject}}.",
{ subject }
)}
/>
</div>
)
@@ -1,5 +1,6 @@
import { Order } from "@medusajs/medusa"
import { useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import Button from "../../../../components/fundamentals/button"
import { AddressPayload } from "../../../../components/templates/address-form"
import { NestedForm } from "../../../../utils/nested-form"
@@ -13,6 +14,7 @@ type Props = {
const ShippingAddressForm = ({ form, order }: Props) => {
const { control, path } = form
const { pushScreen } = useShippingAddressFormScreen()
const { t } = useTranslation()
const {
address_1,
@@ -29,7 +31,9 @@ const ShippingAddressForm = ({ form, order }: Props) => {
return (
<div>
<h2 className="inter-base-semibold">Shipping address</h2>
<h2 className="inter-base-semibold">
{t("shipping-address-form-shipping-address", "Shipping address")}
</h2>
<div className="flex items-center justify-between">
<div className="inter-small-regular text-grey-50">
<p>{`${address_1}${address_2 ? `, ${address_2}` : ""}`}</p>
@@ -49,7 +53,10 @@ const ShippingAddressForm = ({ form, order }: Props) => {
type="button"
onClick={() => pushScreen({ form, order })}
>
Ship to a different address
{t(
"shipping-address-form-ship-to-a-different-address",
"Ship to a different address"
)}
</Button>
</div>
</div>
@@ -2,6 +2,7 @@ import { Order } from "@medusajs/medusa"
import { useAdminRegion } from "medusa-react"
import { useMemo } from "react"
import { useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import Button from "../../../../components/fundamentals/button"
import Modal from "../../../../components/molecules/modal"
import { useLayeredModal } from "../../../../components/molecules/modal/layered-modal"
@@ -17,6 +18,7 @@ type Props = {
}
const ShippingAddressFormScreen = ({ form, order }: Props) => {
const { t } = useTranslation()
const {
resetField,
path,
@@ -82,7 +84,7 @@ const ShippingAddressFormScreen = ({ form, order }: Props) => {
type="button"
onClick={cancelAndPop}
>
Cancel
{t("shipping-address-form-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -91,7 +93,7 @@ const ShippingAddressFormScreen = ({ form, order }: Props) => {
type="button"
onClick={pop}
>
Save and go back
{t("shipping-address-form-save-and-go-back", "Save and go back")}
</Button>
</div>
</Modal.Footer>
@@ -101,10 +103,14 @@ const ShippingAddressFormScreen = ({ form, order }: Props) => {
export const useShippingAddressFormScreen = () => {
const { pop, push } = useLayeredModal()
const { t } = useTranslation()
const pushScreen = (props: Props) => {
push({
title: "Shipping Information",
title: t(
"shipping-address-form-shipping-information",
"Shipping Information"
),
onBack: () => pop(),
view: <ShippingAddressFormScreen {...props} />,
})
@@ -4,6 +4,7 @@ import clsx from "clsx"
import { useAdminShippingOptions } from "medusa-react"
import { useMemo } from "react"
import { Controller, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import PriceFormInput from "../../../../components/forms/general/prices-form/price-form-input"
import Button from "../../../../components/fundamentals/button"
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
@@ -37,6 +38,7 @@ const ShippingForm = ({
isClaim = false,
required = false,
}: Props) => {
const { t } = useTranslation()
const {
control,
path,
@@ -111,20 +113,38 @@ const ShippingForm = ({
<div className="gap-y-base flex flex-col">
<div className="flex flex-col">
<h2 className="inter-base-semibold">
Shipping for {isReturn ? "return" : "replacement"} items
{isReturn
? t(
"shipping-form-shipping-for-return-items",
"Shipping for return items"
)
: t(
"shipping-form-shipping-for-replacement-items",
"Shipping for replacement items"
)}
</h2>
<ShippingFormHelpText isClaim={isClaim} isReturn={isReturn} />
</div>
<Controller
control={control}
name={path("option")}
rules={{ required: required ? `Shipping method is required` : false }}
rules={{
required: required
? t(
"shipping-form-shipping-method-is-required",
"Shipping method is required"
)
: false,
}}
render={({ field: { value, onChange, onBlur, ref, name } }) => {
return (
<NextSelect
ref={ref}
placeholder="Choose shipping method"
label="Shipping method"
placeholder={t(
"shipping-form-choose-shipping-method",
"Choose shipping method"
)}
label={t("shipping-form-shipping-method", "Shipping method")}
name={name}
options={returnShippingOptions}
value={value}
@@ -175,7 +195,7 @@ const ShippingForm = ({
className="h-10"
onClick={setCustomPrice}
>
Add custom price
{t("shipping-form-add-custom-price", "Add custom price")}
</Button>
)}
</div>
@@ -188,13 +208,21 @@ const ShippingFormHelpText = ({
isClaim = false,
isReturn = false,
}: Pick<Props, "isClaim" | "isReturn">) => {
const { t } = useTranslation()
const text = useMemo(() => {
if (isClaim && isReturn) {
return "Return shipping for items claimed by the customer is complimentary."
return t(
"shipping-form-return-shipping-for-items-claimed-by-the-customer-is-complimentary",
"Return shipping for items claimed by the customer is complimentary."
)
}
if (!isReturn) {
return "Shipping for replacement items is complimentary."
return t(
"shipping-form-shipping-for-replacement-items-is-complimentary",
"Shipping for replacement items is complimentary."
)
}
return undefined

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