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

View File

@@ -1,4 +1,5 @@
import React, { useEffect } from "react"
import { useTranslation } from "react-i18next"
import useOutsideClick from "../../../hooks/use-outside-click"
import { usePolling } from "../../../providers/polling-provider"
@@ -8,6 +9,7 @@ import SidedMouthFaceIcon from "../../fundamentals/icons/sided-mouth-face"
import BatchJobActivityList from "../batch-jobs-activity-list"
const ActivityDrawer = ({ onDismiss }) => {
const { t } = useTranslation()
const ref = React.useRef<HTMLDivElement>(null)
const { batchJobs, hasPollingError, refetch } = usePolling()
useOutsideClick(onDismiss, ref)
@@ -21,7 +23,9 @@ const ActivityDrawer = ({ onDismiss }) => {
ref={ref}
className="bg-grey-0 shadow-dropdown rounded-rounded fixed top-[64px] bottom-2 right-3 flex w-[400px] flex-col overflow-x-hidden rounded"
>
<div className="inter-large-semibold pt-7 pl-8 pb-1">Activity</div>
<div className="inter-large-semibold pt-7 pl-8 pb-1">
{t("activity-drawer-activity", "Activity")}
</div>
{!hasPollingError ? (
batchJobs ? (
@@ -37,33 +41,44 @@ const ActivityDrawer = ({ onDismiss }) => {
}
const EmptyActivityDrawer = () => {
const { t } = useTranslation()
return (
<div className="flex h-full w-full flex-col items-center justify-center p-4">
<SidedMouthFaceIcon size={36} />
<span className={"inter-large-semibold text-grey-90 mt-4"}>
It's quite in here...
{t("activity-drawer-no-notifications-title", "It's quiet in here...")}
</span>
<span className={"text-grey-60 inter-base-regular mt-4 text-center"}>
You don't have any notifications at the moment, but once you do they
will live here.
{t(
"activity-drawer-no-notifications-description",
"You don't have any notifications at the moment, but once you do they will live here."
)}
</span>
</div>
)
}
const ErrorActivityDrawer = () => {
const { t } = useTranslation()
return (
<div className="flex h-full w-full flex-col items-center justify-center p-4">
<SadFaceIcon size={36} />
<span className={"inter-large-semibold text-grey-90 mt-4"}>Oh no...</span>
<span className={"inter-large-semibold text-grey-90 mt-4"}>
{t("activity-drawer-error-title", "Oh no...")}
</span>
<span className={"text-grey-60 inter-base-regular mt-2 text-center"}>
Something went wrong while trying to fetch your notifications - We will
keep trying!
{t(
"activity-drawer-error-description",
"Something went wrong while trying to fetch your notifications - We will keep trying!"
)}
</span>
<div className="mt-4 flex items-center">
<Spinner size={"small"} variant={"secondary"} />
<span className="ml-2.5">Processing...</span>
<span className="ml-2.5">
{t("activity-drawer-processing", "Processing...")}
</span>
</div>
</div>
)

View File

@@ -1,6 +1,7 @@
import clsx from "clsx"
import { useEffect } from "react"
import { Controller, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { NestedForm } from "../../../utils/nested-form"
import Switch from "../../atoms/switch"
import InfoIcon from "../../fundamentals/icons/info-icon"
@@ -18,6 +19,7 @@ type Props = {
const AnalyticsConfigForm = ({ form, compact }: Props) => {
const { control, setValue, path } = form
const { t } = useTranslation()
const watchOptOut = useWatch({
control,
@@ -41,13 +43,14 @@ const AnalyticsConfigForm = ({ form, compact }: Props) => {
<div className="gap-y-2xsmall flex flex-1 flex-col">
<div className="flex items-center">
<h2 className="inter-base-semibold mr-2">
Anonymize my usage data{" "}
{t("analytics-config-form-title", "Anonymize my usage data")}
</h2>
{compact && (
<Tooltip
content="You can choose to anonymize your usage data. If this option is
selected, we will not collect your personal information, such as
your name and email address."
content={t(
"analytics-config-form-description",
"You can choose to anonymize your usage data. If this option is selected, we will not collect your personal information, such as your name and email address."
)}
side="top"
>
<InfoIcon size="18px" color={"#889096"} />
@@ -56,9 +59,10 @@ const AnalyticsConfigForm = ({ form, compact }: Props) => {
</div>
{!compact && (
<p className="inter-base-regular text-grey-50">
You can choose to anonymize your usage data. If this option is
selected, we will not collect your personal information, such as
your name and email address.
{t(
"analytics-config-form-description",
"You can choose to anonymize your usage data. If this option is selected, we will not collect your personal information, such as your name and email address."
)}
</p>
)}
</div>
@@ -80,11 +84,17 @@ const AnalyticsConfigForm = ({ form, compact }: Props) => {
<div className="gap-y-2xsmall flex flex-1 flex-col">
<div className="flex items-center">
<h2 className="inter-base-semibold mr-2">
Opt out of sharing my usage data
{t(
"analytics-config-form-opt-out",
"Opt out of sharing my usage data"
)}
</h2>
{compact && (
<Tooltip
content="You can always opt out of sharing your usage data at any time."
content={t(
"analytics-config-form-opt-out-later",
"You can always opt out of sharing your usage data at any time."
)}
side="top"
>
<InfoIcon size="18px" color={"#889096"} />
@@ -93,7 +103,10 @@ const AnalyticsConfigForm = ({ form, compact }: Props) => {
</div>
{!compact && (
<p className="inter-base-regular text-grey-50">
You can always opt out of sharing your usage data at any time.
{t(
"analytics-config-form-opt-out-later",
"You can always opt out of sharing your usage data at any time."
)}
</p>
)}
</div>

View File

@@ -1,5 +1,6 @@
import clsx from "clsx"
import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification"
import { useAnalytics } from "../../../providers/analytics-provider"
import { useAdminCreateAnalyticsConfig } from "../../../services/analytics"
@@ -18,6 +19,7 @@ type AnalyticsPreferenceFormType = {
}
const AnalyticsPreferencesModal = () => {
const { t } = useTranslation()
const notification = useNotification()
const { mutate, isLoading } = useAdminCreateAnalyticsConfig()
@@ -56,8 +58,11 @@ const AnalyticsPreferencesModal = () => {
mutate(config, {
onSuccess: () => {
notification(
"Success",
"Your preferences were successfully updated",
t("analytics-preferences-success", "Success"),
t(
"analytics-preferences-your-preferences-were-successfully-updated",
"Your preferences were successfully updated"
),
"success"
)
@@ -68,7 +73,11 @@ const AnalyticsPreferencesModal = () => {
setSubmittingConfig(false)
},
onError: (err) => {
notification("Error", getErrorMessage(err), "error")
notification(
t("analytics-preferences-error", "Error"),
getErrorMessage(err),
"error"
)
setSubmittingConfig(false)
},
})
@@ -80,27 +89,29 @@ const AnalyticsPreferencesModal = () => {
<div className="flex flex-col items-center">
<div className="mt-5xlarge flex w-full max-w-[664px] flex-col">
<h1 className="inter-xlarge-semibold mb-large">
Help us get better
{t(
"analytics-preferences-help-us-get-better",
"Help us get better"
)}
</h1>
<p className="text-grey-50">
To create the most compelling e-commerce experience we would like
to gain insights in how you use Medusa. User insights allow us to
build a better, more engaging, and more usable products. We only
collect data for product improvements. Read what data we gather in
our{" "}
{t(
"analytics-preferences-disclaimer",
"To create the most compelling e-commerce experience we would like to gain insights in how you use Medusa. User insights allow us to build a better, more engaging, and more usable products. We only collect data for product improvements. Read what data we gather in our"
)}{" "}
<a
href="https://docs.medusajs.com/usage"
rel="noreferrer noopener"
target="_blank"
className="text-violet-60"
>
documentation
{t("analytics-preferences-documentation", "documentation")}
</a>
.
</p>
<div className="mt-xlarge gap-y-xlarge flex flex-col">
<InputField
label="Email"
label={"Email"}
placeholder="you@company.com"
disabled={watchOptOut || watchAnonymize}
className={clsx("transition-opacity", {
@@ -108,7 +119,10 @@ const AnalyticsPreferencesModal = () => {
})}
{...register("email", {
pattern: {
message: "Please enter a valid email",
message: t(
"analytics-preferences-please-enter-a-valid-email",
"Please enter a valid email"
),
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
},
})}
@@ -123,7 +137,7 @@ const AnalyticsPreferencesModal = () => {
loading={isLoading}
onClick={onSubmit}
>
Continue
{t("analytics-preferences-continue", "Continue")}
</Button>
</div>
</div>

View File

@@ -18,6 +18,7 @@ import PlusIcon from "../../fundamentals/icons/plus-icon"
import InputHeader from "../../fundamentals/input-header"
import Input from "../../molecules/input"
import Select from "../../molecules/select"
import { useTranslation } from "react-i18next"
type CurrencyInputProps = {
currencyCodes?: string[]
@@ -74,6 +75,7 @@ const Root: React.FC<CurrencyInputProps> = ({
label: code.toUpperCase(),
value: code,
})) ?? []
const { t } = useTranslation()
const [selectedCurrency, setSelectedCurrency] = useState<
CurrencyType | undefined
@@ -132,7 +134,7 @@ const Root: React.FC<CurrencyInputProps> = ({
{!readOnly ? (
<Select
enableSearch
label="Currency"
label={t("currency-input-currency", "Currency")}
value={value}
onChange={onCurrencyChange}
options={options}
@@ -140,7 +142,7 @@ const Root: React.FC<CurrencyInputProps> = ({
/>
) : (
<Input
label="Currency"
label={t("currency-input-currency", "Currency")}
value={value?.label}
readOnly
className="pointer-events-none"
@@ -172,6 +174,8 @@ const Amount = forwardRef<HTMLInputElement, AmountInputProps>(
}: AmountInputProps,
ref
) => {
const { t } = useTranslation()
const { currencyInfo } = useContext(CurrencyContext)
const [invalid, setInvalid] = useState<boolean>(false)
const [formattedValue, setFormattedValue] = useState<string | undefined>(
@@ -250,7 +254,10 @@ const Amount = forwardRef<HTMLInputElement, AmountInputProps>(
<Tooltip
open={invalid}
side={"top"}
content={invalidMessage || "Amount is not valid"}
content={
invalidMessage ||
t("currency-input-amount-is-not-valid", "Amount is not valid")
}
>
<span className="inter-base-regular text-grey-40 mr-xsmall">
{currencyInfo.symbol_native}

View File

@@ -1,4 +1,5 @@
import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import useNotification from "../../hooks/use-notification"
import { getErrorMessage } from "../../utils/error-messages"
@@ -16,14 +17,15 @@ type DeletePromptProps = {
}
const DeletePrompt: React.FC<DeletePromptProps> = ({
heading = "Are you sure you want to delete?",
heading,
text = "",
successText = "Delete successful",
cancelText = "No, cancel",
confirmText = "Yes, remove",
successText,
cancelText,
confirmText,
handleClose,
onDelete,
}) => {
const { t } = useTranslation()
const notification = useNotification()
const [isLoading, setIsLoading] = useState(false)
@@ -34,7 +36,12 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
onDelete()
.then(() => {
if (successText) {
notification("Success", successText, "success")
notification(
t("organisms-success", "Success"),
successText ||
t("organisms-delete-successful", "Delete successful"),
"success"
)
}
})
.catch((err) => notification("Error", getErrorMessage(err), "error"))
@@ -49,7 +56,13 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
<Modal.Body>
<Modal.Content>
<div className="flex flex-col">
<span className="inter-large-semibold">{heading}</span>
<span className="inter-large-semibold">
{heading ||
t(
"organisms-are-you-sure-you-want-to-delete",
"Are you sure you want to delete?"
)}
</span>
<span className="inter-base-regular text-grey-50 mt-1">{text}</span>
</div>
</Modal.Content>
@@ -61,7 +74,7 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
size="small"
onClick={handleClose}
>
{cancelText}
{cancelText || t("organisms-no-cancel", "No, cancel")}
</Button>
<Button
loading={isLoading}
@@ -71,7 +84,7 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
onClick={handleSubmit}
disabled={isLoading}
>
{confirmText}
{confirmText || t("organisms-yes-remove", "Yes, remove")}
</Button>
</div>
</Modal.Footer>

View File

@@ -1,6 +1,7 @@
import * as RadixCollapsible from "@radix-ui/react-collapsible"
import clsx from "clsx"
import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import ArrowDownIcon from "../../fundamentals/icons/arrow-down-icon"
import ArrowUpIcon from "../../fundamentals/icons/arrow-up-icon"
@@ -17,10 +18,19 @@ const DetailsCollapsible = ({
contentProps,
children,
}: DetailsCollapsibleProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const Icon = open ? ArrowUpIcon : ArrowDownIcon
const label = open ? "Hide additional details" : "Show additional details"
const label = open
? t(
"details-collapsible-hide-additional-details",
"Hide additional details"
)
: t(
"details-collapsible-show-additional-details",
"Show additional details"
)
return (
<RadixCollapsible.Root

View File

@@ -2,6 +2,7 @@ import { User } from "@medusajs/medusa"
import { useAdminUpdateUser } from "medusa-react"
import React, { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages"
import FormValidator from "../../../utils/form-validator"
@@ -33,6 +34,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
formState: { errors },
} = useForm<EditUserModalFormData>()
const notification = useNotification()
const { t } = useTranslation()
useEffect(() => {
reset(mapUser(user))
@@ -41,11 +43,19 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
const onSubmit = (data: EditUserModalFormData) => {
mutate(data, {
onSuccess: () => {
notification("Success", `User was updated`, "success")
notification(
t("edit-user-modal-success", "Success"),
t("edit-user-modal-user-was-updated", "User was updated"),
"success"
)
onSuccess()
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("edit-user-modal-error", "Error"),
getErrorMessage(error),
"error"
)
},
onSettled: () => {
handleClose()
@@ -58,13 +68,18 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
<form onSubmit={handleSubmit(onSubmit)}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<span className="inter-xlarge-semibold">Edit User</span>
<span className="inter-xlarge-semibold">
{t("edit-user-modal-edit-user", "Edit User")}
</span>
</Modal.Header>
<Modal.Content>
<div className="gap-large mb-base grid w-full grid-cols-2">
<InputField
label="First Name"
placeholder="First name..."
label={t("edit-user-modal-first-name-label", "First Name")}
placeholder={t(
"edit-user-modal-first-name-placeholder",
"First name..."
)}
required
{...register("first_name", {
required: FormValidator.required("First name"),
@@ -74,8 +89,11 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
errors={errors}
/>
<InputField
label="Last Name"
placeholder="Last name..."
label={t("edit-user-modal-last-name-label", "Last Name")}
placeholder={t(
"edit-user-modal-last-name-placeholder",
"Last name..."
)}
required
{...register("last_name", {
required: FormValidator.required("Last name"),
@@ -85,7 +103,11 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
errors={errors}
/>
</div>
<InputField label="Email" disabled value={user.email} />
<InputField
label={t("edit-user-modal-email", "Email")}
disabled
value={user.email}
/>
</Modal.Content>
<Modal.Footer>
<div className="flex w-full justify-end">
@@ -95,7 +117,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
onClick={handleClose}
className="mr-2"
>
Cancel
{t("edit-user-modal-cancel", "Cancel")}
</Button>
<Button
loading={isLoading}
@@ -103,7 +125,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
variant="primary"
size="small"
>
Save
{t("edit-user-modal-save", "Save")}
</Button>
</div>
</Modal.Footer>

View File

@@ -1,9 +1,11 @@
import { AxiosError } from "axios"
import React, { ErrorInfo } from "react"
import { analyticsOptIn } from "../../../services/analytics"
import { Translation } from "react-i18next"
import Button from "../../fundamentals/button"
import { WRITE_KEY } from "../../../constants/analytics"
import { AnalyticsBrowser } from "@segment/analytics-next"
import { TFunction } from "i18next"
type State = {
hasError: boolean
@@ -16,7 +18,7 @@ type Props = {
}
// Analytics instance used for tracking errors
let analyticsInstance: ReturnType<typeof AnalyticsBrowser.load> | undefined;
let analyticsInstance: ReturnType<typeof AnalyticsBrowser.load> | undefined
const analytics = () => {
if (!analyticsInstance) {
@@ -75,10 +77,14 @@ class ErrorBoundary extends React.Component<Props, State> {
</p>
)}
<h1 className="inter-xlarge-semibold mb-xsmall">
{errorMessage(this.state.status)}
<Translation>
{(t) => errorMessage(t, this.state.status)}
</Translation>
</h1>
<p className="inter-base-regular text-grey-50">
{errorDescription(this.state.status)}
<Translation>
{(t) => errorDescription(t, this.state.status)}
</Translation>
</p>
</div>
@@ -88,7 +94,11 @@ class ErrorBoundary extends React.Component<Props, State> {
variant="primary"
onClick={this.dismissError}
>
Back to dashboard
<Translation>
{(t) =>
t("error-boundary-back-to-dashboard", "Back to dashboard")
}
</Translation>
</Button>
</div>
</div>
@@ -112,43 +122,72 @@ const shouldTrackEvent = async (error: Error) => {
return false
}
return await analyticsOptIn();
return await analyticsOptIn()
}
const errorMessage = (status?: number) => {
const defaultMessage = "An unknown error occured"
const errorMessage = (t: TFunction, status?: number) => {
const defaultMessage = t(
"error-boundary-an-unknown-error-occured",
"An unknown error occured"
)
if (!status) {
return defaultMessage
}
const message = {
400: "Bad request",
401: "You are not logged in",
403: "You do not have permission perform this action",
404: "Page was not found",
500: "An unknown server error occured",
503: "Server is currently unavailable",
400: t("error-boundary-bad-request", "Bad request"),
401: t("error-boundary-you-are-not-logged-in", "You are not logged in"),
403: t(
"error-boundary-you-do-not-have-permission-perform-this-action",
"You do not have permission perform this action"
),
404: t("error-boundary-page-was-not-found", "Page was not found"),
500: t(
"error-boundary-an-unknown-server-error-occured",
"An unknown server error occured"
),
503: t("error-boundary-503", "Server is currently unavailable"),
}[status]
return message || defaultMessage
}
const errorDescription = (status?: number) => {
const defaultDescription =
const errorDescription = (t: TFunction, status?: number) => {
const defaultDescription = t(
"error-boundary-500",
"An error occurred with unspecified causes, this is most likely due to a techinical issue on our end. Please try refreshing the page. If the issue keeps happening, contact your administrator."
)
if (!status) {
return defaultDescription
}
const description = {
400: "The request was malformed, fix your request and please try again.",
401: "You are not logged in, please log in to proceed.",
403: "You do not have permission perform this action, if you think this is a mistake, contact your administrator.",
404: "The page you have requested was not found, please check the URL and try again.",
500: "The server was not able to handle your request, this is mostly likely due to a techinical issue on our end. Please try again. If the issue keeps happening, contact your administrator.",
503: "The server is temporarily unavailable, and your request could not be processed. Please try again later. If the issue keeps happening, contact your administrator.",
400: t(
"error-boundary-400",
"The request was malformed, fix your request and please try again."
),
401: t(
"error-boundary-401",
"You are not logged in, please log in to proceed."
),
403: t(
"error-boundary-403",
"You do not have permission perform this action, if you think this is a mistake, contact your administrator."
),
404: t(
"error-boundary-404",
"The page you have requested was not found, please check the URL and try again."
),
500: t(
"error-boundary-500-2",
"The server was not able to handle your request, this is mostly likely due to a techinical issue on our end. Please try again. If the issue keeps happening, contact your administrator."
),
503: t(
"error-boundary-503-2",
"The server is temporarily unavailable, and your request could not be processed. Please try again later. If the issue keeps happening, contact your administrator."
),
}[status]
return description || defaultDescription

View File

@@ -1,4 +1,5 @@
import React from "react"
import { useTranslation } from "react-i18next"
import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal"
@@ -15,6 +16,7 @@ const ExportModal: React.FC<ExportModalProps> = ({
loading,
onSubmit,
}) => {
const { t } = useTranslation()
return (
<Modal handleClose={handleClose}>
<Modal.Body>
@@ -30,7 +32,7 @@ const ExportModal: React.FC<ExportModalProps> = ({
overview.
</div> */}
<div className="inter-small-regular text-grey-50 mb-4 flex">
Initialize an export of your data
{t("export-modal-title", "Initialize an export of your data")}
</div>
</Modal.Content>
<Modal.Footer>
@@ -41,7 +43,7 @@ const ExportModal: React.FC<ExportModalProps> = ({
onClick={handleClose}
className="mr-2"
>
Cancel
{t("export-modal-cancel", "Cancel")}
</Button>
<Button
loading={loading}
@@ -50,7 +52,7 @@ const ExportModal: React.FC<ExportModalProps> = ({
size="small"
onClick={onSubmit}
>
Export
{t("export-modal-export", "Export")}
</Button>
</div>
</Modal.Footer>

View File

@@ -1,4 +1,5 @@
import React from "react"
import { useTranslation } from "react-i18next"
import FileUploadField from "../../atoms/file-upload-field"
import Modal from "../../molecules/modal"
@@ -13,11 +14,14 @@ const FileUploadModal: React.FC<FileUploadModalProps> = ({
filetypes,
setFiles,
}) => {
const { t } = useTranslation()
return (
<Modal handleClose={handleClose}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<span className="inter-xlarge-semibold">Upload a new photo</span>
<span className="inter-xlarge-semibold">
{t("file-upload-modal-upload-a-new-photo", "Upload a new photo")}
</span>
</Modal.Header>
<Modal.Content>
<div className="h-96">

View File

@@ -1,4 +1,5 @@
import React, { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { normalizeAmount } from "../../../utils/prices"
import EditIcon from "../../fundamentals/icons/edit-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon"
@@ -6,7 +7,7 @@ import UnpublishIcon from "../../fundamentals/icons/unpublish-icon"
import StatusIndicator from "../../fundamentals/status-indicator"
import { ActionType } from "../../molecules/actionables"
import BannerCard from "../../molecules/banner-card"
import TagGrid from "../../molecules/tag-grid.tsx"
import TagGrid from "../../molecules/tag-grid"
type GiftCardVariant = {
prices: {
@@ -38,19 +39,23 @@ const GiftCardBanner: React.FC<GiftCardBannerProps> = ({
onUnpublish,
onDelete,
}) => {
const { t } = useTranslation()
const actions: ActionType[] = [
{
label: "Edit",
label: t("gift-card-banner-edit", "Edit"),
onClick: onEdit,
icon: <EditIcon size={16} />,
},
{
label: status === "published" ? "Unpublish" : "Publish",
label:
status === "published"
? t("gift-card-banner-unpublish", "Unpublish")
: t("gift-card-banner-publish", "Publish"),
onClick: onUnpublish,
icon: <UnpublishIcon size={16} />,
},
{
label: "Delete",
label: t("gift-card-banner-delete", "Delete"),
onClick: onDelete,
icon: <TrashIcon size={16} />,
variant: "danger",
@@ -84,7 +89,11 @@ const GiftCardBanner: React.FC<GiftCardBannerProps> = ({
<TagGrid tags={denominations} badgeVariant="default" />
<StatusIndicator
variant={status === "published" ? "success" : "danger"}
title={status === "published" ? "Published" : "Unpublished"}
title={
status === "published"
? t("gift-card-banner-published", "Published")
: t("gift-card-banner-unpublished", "Unpublished")
}
/>
</div>
</BannerCard.Footer>

View File

@@ -2,6 +2,7 @@ import { Product } from "@medusajs/medusa"
import { useAdminCreateVariant, useAdminStore } from "medusa-react"
import { useCallback, useMemo } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form"
@@ -31,6 +32,7 @@ type AddDenominationModalFormType = {
}
const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
const { t } = useTranslation()
const { mutate, isLoading: isMutating } = useAdminCreateVariant(giftCard.id)
const { store } = useAdminStore()
@@ -114,8 +116,14 @@ const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
mutate(payload, {
onSuccess: () => {
notification(
"Denomination added",
"A new denomination was successfully added",
t(
"gift-card-denominations-section-denomination-added",
"Denomination added"
),
t(
"gift-card-denominations-section-a-new-denomination-was-successfully-added",
"A new denomination was successfully added"
),
"success"
)
handleClose()
@@ -124,13 +132,20 @@ const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
const errorMessage = () => {
// @ts-ignore
if (error.response?.data?.type === "duplicate_error") {
return `A denomination with that default value already exists`
return t(
"gift-card-denominations-section-a-denomination-with-that-default-value-already-exists",
"A denomination with that default value already exists"
)
} else {
return getErrorMessage(error)
}
}
notification("Error", errorMessage(), "error")
notification(
t("gift-card-denominations-section-error", "Error"),
errorMessage(),
"error"
)
},
})
})
@@ -139,7 +154,12 @@ const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
<Modal open={open} handleClose={handleClose}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<h1 className="inter-xlarge-semibold">Add Denomination</h1>
<h1 className="inter-xlarge-semibold">
{t(
"gift-card-denominations-section-add-denomination",
"Add Denomination"
)}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
@@ -153,7 +173,7 @@ const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
type="button"
onClick={handleClose}
>
Cancel
{t("gift-card-denominations-section-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -162,7 +182,10 @@ const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
disabled={isMutating || !isDirty}
loading={isMutating}
>
Save and close
{t(
"gift-card-denominations-section-save-and-close",
"Save and close"
)}
</Button>
</div>
</Modal.Footer>

View File

@@ -7,6 +7,7 @@ import {
} from "medusa-react"
import { useCallback, useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form"
@@ -31,6 +32,7 @@ const EditDenominationsModal = ({
onClose,
open,
}: EditDenominationsModalProps) => {
const { t } = useTranslation()
const { store } = useAdminStore()
const { mutate, isLoading } = useAdminUpdateVariant(denomination.product_id)
@@ -91,15 +93,25 @@ const EditDenominationsModal = ({
{
onSuccess: () => {
notification(
"Denomination updated",
"A new denomination was successfully updated",
t(
"gift-card-denominations-section-denomination-updated",
"Denomination updated"
),
t(
"gift-card-denominations-section-a-new-denomination-was-successfully-updated",
"A new denomination was successfully updated"
),
"success"
)
queryClient.invalidateQueries(adminProductKeys.all)
handleClose()
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("gift-card-denominations-section-error", "Error"),
getErrorMessage(error),
"error"
)
},
}
)
@@ -109,7 +121,12 @@ const EditDenominationsModal = ({
<Modal open={open} handleClose={handleClose}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<h1 className="inter-xlarge-semibold">Edit Denomination</h1>
<h1 className="inter-xlarge-semibold">
{t(
"gift-card-denominations-section-edit-denomination",
"Edit Denomination"
)}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
@@ -123,7 +140,7 @@ const EditDenominationsModal = ({
type="button"
onClick={handleClose}
>
Cancel
{t("gift-card-denominations-section-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -132,7 +149,10 @@ const EditDenominationsModal = ({
loading={isLoading}
disabled={!isDirty || isLoading}
>
Save and close
{t(
"gift-card-denominations-section-save-and-close",
"Save and close"
)}
</Button>
</div>
</Modal.Footer>

View File

@@ -1,4 +1,5 @@
import { Product } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import useToggleState from "../../../hooks/use-toggle-state"
import PlusIcon from "../../fundamentals/icons/plus-icon"
import Section from "../section"
@@ -12,6 +13,7 @@ type GiftCardDenominationsSectionProps = {
const GiftCardDenominationsSection = ({
giftCard,
}: GiftCardDenominationsSectionProps) => {
const { t } = useTranslation()
const {
state: addDenomination,
close: closeAddDenomination,
@@ -21,11 +23,17 @@ const GiftCardDenominationsSection = ({
return (
<>
<Section
title="Denominations"
title={t(
"gift-card-denominations-section-denominations",
"Denominations"
)}
forceDropdown
actions={[
{
label: "Add Denomination",
label: t(
"gift-card-denominations-section-add-denomination",
"Add Denomination"
),
onClick: openAddDenomination,
icon: <PlusIcon size={20} />,
},

View File

@@ -2,6 +2,7 @@ import { MoneyAmount, ProductVariant } from "@medusajs/medusa"
import { createColumnHelper } from "@tanstack/react-table"
import { useAdminDeleteVariant, useAdminStore } from "medusa-react"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import useImperativeDialog from "../../../hooks/use-imperative-dialog"
import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state"
@@ -17,6 +18,7 @@ const columnHelper = createColumnHelper<ProductVariant>()
export const useDenominationColumns = () => {
const { store } = useAdminStore()
const { t } = useTranslation()
const columns = useMemo(() => {
if (!store) {
@@ -27,7 +29,10 @@ export const useDenominationColumns = () => {
return [
columnHelper.display({
header: "Denomination",
header: t(
"gift-card-denominations-section-denomination",
"Denomination"
),
id: "denomination",
cell: ({ row }) => {
const defaultDenomination = row.original.prices.find(
@@ -50,7 +55,10 @@ export const useDenominationColumns = () => {
},
}),
columnHelper.display({
header: "In other currencies",
header: t(
"gift-card-denominations-section-in-other-currencies",
"In other currencies"
),
id: "other_currencies",
cell: ({ row }) => {
const otherCurrencies = row.original.prices.filter(
@@ -98,7 +106,13 @@ export const useDenominationColumns = () => {
</ul>
}
>
<span className="text-grey-50 cursor-default">{`, and ${remainder.length} more`}</span>
<span className="text-grey-50 cursor-default">
{t(
"gift-card-denominations-section-and-more",
", and {{count}} more",
{ count: remainder.length }
)}
</span>
</Tooltip>
)}
</p>
@@ -118,6 +132,7 @@ export const useDenominationColumns = () => {
}
const Actions = ({ original }: { original: ProductVariant }) => {
const { t } = useTranslation()
const { state, open, close } = useToggleState()
const { mutateAsync } = useAdminDeleteVariant(original.product_id)
@@ -127,21 +142,37 @@ const Actions = ({ original }: { original: ProductVariant }) => {
const onDelete = async () => {
const shouldDelete = await dialog({
heading: "Delete denomination",
text: "Are you sure you want to delete this denomination?",
heading: t(
"gift-card-denominations-section-delete-denomination",
"Delete denomination"
),
text: t(
"gift-card-denominations-section-confirm-delete",
"Are you sure you want to delete this denomination?"
),
})
if (shouldDelete) {
mutateAsync(original.id, {
onSuccess: () => {
notification(
"Denomination deleted",
"Denomination was successfully deleted",
t(
"gift-card-denominations-section-denomination-deleted",
"Denomination deleted"
),
t(
"gift-card-denominations-section-denomination-was-successfully-deleted",
"Denomination was successfully deleted"
),
"success"
)
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("gift-card-denominations-section-error", "Error"),
getErrorMessage(error),
"error"
)
},
})
}
@@ -149,12 +180,12 @@ const Actions = ({ original }: { original: ProductVariant }) => {
const actions: ActionType[] = [
{
label: "Edit",
label: t("gift-card-denominations-section-edit", "Edit"),
onClick: open,
icon: <EditIcon size={20} />,
},
{
label: "Delete",
label: t("gift-card-denominations-section-delete", "Delete"),
onClick: onDelete,
icon: <TrashIcon size={20} />,
variant: "danger",

View File

@@ -1,4 +1,5 @@
import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../fundamentals/button"
import DiscordIcon from "../../fundamentals/icons/discord-icon"
import InputField from "../../molecules/input"
@@ -12,6 +13,7 @@ type MailDialogProps = {
}
const MailDialog = ({ open, onClose }: MailDialogProps) => {
const { t } = useTranslation()
const [subject, setSubject] = useState("")
const [body, setBody] = useState("")
const [link, setLink] = useState("mailto:support@medusajs.com")
@@ -30,21 +32,30 @@ const MailDialog = ({ open, onClose }: MailDialogProps) => {
<Dialog.Content className="bg-grey-0 shadow-dropdown rounded-rounded fixed top-[64px] bottom-2 right-3 flex w-[400px] flex-col justify-between p-8">
<div>
<Dialog.Title className="inter-xlarge-semibold mb-1">
How can we help?
{t("help-dialog-how-can-we-help", "How can we help?")}
</Dialog.Title>
<Dialog.Description className="inter-small-regular text-grey-50 mb-6">
We usually respond in a few hours
{t(
"help-dialog-we-usually-respond-in-a-few-hours",
"We usually respond in a few hours"
)}
</Dialog.Description>
<InputField
label={"Subject"}
label={t("help-dialog-subject", "Subject")}
value={subject}
className="mb-4"
placeholder="What is it about?..."
placeholder={t(
"help-dialog-what-is-it-about",
"What is it about?..."
)}
onChange={(e) => setSubject(e.target.value)}
/>
<TextArea
label={"How can we help?"}
placeholder="Write a message..."
label={t("help-dialog-how-can-we-help", "How can we help?")}
placeholder={t(
"help-dialog-write-a-message",
"Write a message..."
)}
value={body}
onChange={(e) => {
setBody(e.target.value)
@@ -65,15 +76,21 @@ const MailDialog = ({ open, onClose }: MailDialogProps) => {
<DiscordIcon size={24} />
</span>
<p className="text-grey-40 inter-small-regular text-center leading-6">
Feel free to join our community of
{t(
"help-dialog-feel-free-to-join-our-community-of",
"Feel free to join our community of"
)}
<br />
merchants and e-commerce developers
{t(
"help-dialog-merchants-and-e-commerce-developers",
"merchants and e-commerce developers"
)}
</p>
</div>
</a>
<a className="w-full" href={link}>
<Button variant="primary" size="large" className="w-full">
Send a message
{t("help-dialog-send-a-message", "Send a message")}
</Button>
</a>
</div>

View File

@@ -1,6 +1,7 @@
import { useAdminCreateInvite } from "medusa-react"
import React from "react"
import { Controller, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification"
import { Role } from "../../../types/shared"
import { getErrorMessage } from "../../../utils/error-messages"
@@ -20,6 +21,7 @@ type InviteModalFormData = {
const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
const notification = useNotification()
const { t } = useTranslation()
const { mutate, isLoading } = useAdminCreateInvite()
@@ -33,20 +35,34 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
},
{
onSuccess: () => {
notification("Success", `Invitation sent to ${data.user}`, "success")
notification(
t("invite-modal-success", "Success"),
t(
"invite-modal-invitation-sent-to",
"Invitation sent to {{user}}",
{
user: data.user,
}
),
"success"
)
handleClose()
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("invite-modal-error", "Error"),
getErrorMessage(error),
"error"
)
},
}
)
}
const roleOptions: Role[] = [
{ value: "member", label: "Member" },
{ value: "admin", label: "Admin" },
{ value: "developer", label: "Developer" },
{ value: "member", label: t("invite-modal-member", "Member") },
{ value: "admin", label: t("invite-modal-admin", "Admin") },
{ value: "developer", label: t("invite-modal-developer", "Developer") },
]
return (
@@ -54,12 +70,14 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
<form onSubmit={handleSubmit(onSubmit)}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<span className="inter-xlarge-semibold">Invite Users</span>
<span className="inter-xlarge-semibold">
{t("invite-modal-invite-users", "Invite Users")}
</span>
</Modal.Header>
<Modal.Content>
<div className="gap-y-base flex flex-col">
<InputField
label="Email"
label={t("invite-modal-email", "Email")}
placeholder="lebron@james.com"
required
{...register("user", { required: true })}
@@ -67,12 +85,15 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
<Controller
name="role"
control={control}
defaultValue={{ label: "Member", value: "member" }}
defaultValue={{
label: t("invite-modal-member", "Member"),
value: "member",
}}
render={({ field: { value, onChange, onBlur, ref } }) => {
return (
<NextSelect
label="Role"
placeholder="Select role"
label={t("invite-modal-role", "Role")}
placeholder={t("invite-modal-select-role", "Select role")}
onBlur={onBlur}
ref={ref}
onChange={onChange}
@@ -93,7 +114,7 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
type="button"
onClick={handleClose}
>
Cancel
{t("invite-modal-cancel", "Cancel")}
</Button>
<Button
loading={isLoading}
@@ -102,7 +123,7 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
className="text-small w-32 justify-center"
variant="primary"
>
Invite
{t("invite-modal-invite", "Invite")}
</Button>
</div>
</Modal.Footer>

View File

@@ -2,6 +2,7 @@ import { useAdminLogin } from "medusa-react"
import { useForm } from "react-hook-form"
import { useNavigate } from "react-router-dom"
import { useWidgets } from "../../../providers/widget-provider"
import { useTranslation } from "react-i18next"
import InputError from "../../atoms/input-error"
import WidgetContainer from "../../extensions/widget-container"
import Button from "../../fundamentals/button"
@@ -25,6 +26,7 @@ const LoginCard = ({ toResetPassword }: LoginCardProps) => {
} = useForm<FormValues>()
const navigate = useNavigate()
const { mutate, isLoading } = useAdminLogin()
const { t } = useTranslation()
const { getWidgets } = useWidgets()
@@ -38,7 +40,10 @@ const LoginCard = ({ toResetPassword }: LoginCardProps) => {
"password",
{
type: "manual",
message: "These credentials do not match our records.",
message: t(
"login-card-no-match",
"These credentials do not match our records."
),
},
{
shouldFocus: true,
@@ -62,17 +67,17 @@ const LoginCard = ({ toResetPassword }: LoginCardProps) => {
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col items-center">
<h1 className="inter-xlarge-semibold text-grey-90 mb-large text-[20px]">
Log in to Medusa
{t("login-card-log-in-to-medusa", "Log in to Medusa")}
</h1>
<div>
<SigninInput
placeholder="Email"
placeholder={t("login-card-email", "Email")}
{...register("email", { required: true })}
autoComplete="email"
className="mb-small"
/>
<SigninInput
placeholder="Password"
placeholder={t("login-card-password", "Password")}
type={"password"}
{...register("password", { required: true })}
autoComplete="current-password"
@@ -93,7 +98,7 @@ const LoginCard = ({ toResetPassword }: LoginCardProps) => {
className="inter-small-regular text-grey-50 mt-8 cursor-pointer"
onClick={toResetPassword}
>
Forgot your password?
{t("login-card-forgot-your-password", "Forgot your password?")}
</span>
</div>
</form>

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../fundamentals/button"
import PlusIcon from "../../fundamentals/icons/plus-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon"
@@ -20,6 +21,7 @@ const Metadata: React.FC<AddMetadataProps> = ({
setMetadata,
heading = "Metadata",
}) => {
const { t } = useTranslation()
const [localData, setLocalData] = useState<MetadataField[]>([])
useEffect(() => {
@@ -79,7 +81,7 @@ const Metadata: React.FC<AddMetadataProps> = ({
onClick={addKeyPair}
>
<PlusIcon size={20} />
Add Metadata
{t("metadata-add-metadata", "Add Metadata")}
</Button>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { Product } from "@medusajs/medusa"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { countries } from "../../../utils/countries"
import { nestedForm } from "../../../utils/nested-form"
@@ -23,6 +24,7 @@ type AttributesForm = {
}
const AttributeModal = ({ product, open, onClose }: Props) => {
const { t } = useTranslation()
const { onUpdate, updating } = useEditProductActions(product.id)
const form = useForm<AttributesForm>({
defaultValues: getDefaultValues(product),
@@ -67,21 +69,33 @@ const AttributeModal = ({ product, open, onClose }: Props) => {
<Modal open={open} handleClose={onReset} isLargeModal>
<Modal.Body>
<Modal.Header handleClose={onReset}>
<h1 className="inter-xlarge-semibold m-0">Edit Attributes</h1>
<h1 className="inter-xlarge-semibold m-0">
{t("product-attributes-section-edit-attributes", "Edit Attributes")}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
<div className="mb-xlarge">
<h2 className="inter-large-semibold mb-2xsmall">Dimensions</h2>
<h2 className="inter-large-semibold mb-2xsmall">
{t("product-attributes-section-dimensions", "Dimensions")}
</h2>
<p className="inter-base-regular text-grey-50 mb-large">
Configure to calculate the most accurate shipping rates
{t(
"product-attributes-section-configure-to-calculate-the-most-accurate-shipping-rates",
"Configure to calculate the most accurate shipping rates"
)}
</p>
<DimensionsForm form={nestedForm(form, "dimensions")} />
</div>
<div>
<h2 className="inter-large-semibold mb-2xsmall">Customs</h2>
<h2 className="inter-large-semibold mb-2xsmall">
{t("product-attributes-section-customs", "Customs")}
</h2>
<p className="inter-base-regular text-grey-50 mb-large">
Configure to calculate the most accurate shipping rates
{t(
"product-attributes-section-configure-to-calculate-the-most-accurate-shipping-rates",
"Configure to calculate the most accurate shipping rates"
)}
</p>
<CustomsForm form={nestedForm(form, "customs")} />
</div>
@@ -94,7 +108,7 @@ const AttributeModal = ({ product, open, onClose }: Props) => {
type="button"
onClick={onReset}
>
Cancel
{t("product-attributes-section-cancel", "Cancel")}
</Button>
<Button
size="small"
@@ -103,7 +117,7 @@ const AttributeModal = ({ product, open, onClose }: Props) => {
disabled={!isDirty}
loading={updating}
>
Save
{t("product-attributes-section-save", "Save")}
</Button>
</div>
</Modal.Footer>

View File

@@ -1,4 +1,5 @@
import { Product } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import useToggleState from "../../../hooks/use-toggle-state"
import EditIcon from "../../fundamentals/icons/edit-icon"
import { ActionType } from "../../molecules/actionables"
@@ -10,6 +11,7 @@ type Props = {
}
const ProductAttributesSection = ({ product }: Props) => {
const { t } = useTranslation()
const { state, toggle, close } = useToggleState()
const actions: ActionType[] = [
@@ -24,7 +26,9 @@ const ProductAttributesSection = ({ product }: Props) => {
<>
<Section title="Attributes" actions={actions} forceDropdown>
<div className="gap-y-xsmall mb-large mt-base flex flex-col">
<h2 className="inter-base-semibold">Dimensions</h2>
<h2 className="inter-base-semibold">
{t("product-attributes-section-dimensions", "Dimensions")}
</h2>
<div className="gap-y-xsmall flex flex-col">
<Attribute attribute="Height" value={product.height} />
<Attribute attribute="Width" value={product.width} />
@@ -33,12 +37,23 @@ const ProductAttributesSection = ({ product }: Props) => {
</div>
</div>
<div className="gap-y-xsmall flex flex-col">
<h2 className="inter-base-semibold">Customs</h2>
<h2 className="inter-base-semibold">
{t("product-attributes-section-customs", "Customs")}
</h2>
<div className="gap-y-xsmall flex flex-col">
<Attribute attribute="MID Code" value={product.mid_code} />
<Attribute attribute="HS Code" value={product.hs_code} />
<Attribute
attribute="Country of origin"
attribute={t("product-attributes-section-mid-code", "MID Code")}
value={product.mid_code}
/>
<Attribute
attribute={t("product-attributes-section-hs-code", "HS Code")}
value={product.hs_code}
/>
<Attribute
attribute={t(
"product-attributes-section-country-of-origin",
"Country of origin"
)}
value={product.origin_country}
/>
</div>

View File

@@ -1,5 +1,6 @@
import { Product, SalesChannel } from "@medusajs/medusa"
import { useAdminUpdateProduct } from "medusa-react"
import { useTranslation } from "react-i18next"
import SalesChannelsModal from "../../forms/product/sales-channels-modal"
import useNotification from "../../../hooks/use-notification"
@@ -11,6 +12,7 @@ type Props = {
const ChannelsModal = ({ product, open, onClose }: Props) => {
const notification = useNotification()
const { t } = useTranslation()
const { mutateAsync } = useAdminUpdateProduct(product.id)
@@ -19,9 +21,23 @@ const ChannelsModal = ({ product, open, onClose }: Props) => {
await mutateAsync({
sales_channels: channels.map((c) => ({ id: c.id })),
})
notification("Success", "Successfully updated sales channels", "success")
notification(
t("product-general-section-success", "Success"),
t(
"product-general-section-successfully-updated-sales-channels",
"Successfully updated sales channels"
),
"success"
)
} catch (e) {
notification("Error", "Failed to update sales channels", "error")
notification(
t("product-general-section-error", "Error"),
t(
"product-general-section-failed-to-update-sales-channels",
"Failed to update sales channels"
),
"error"
)
}
}

View File

@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next"
import DiscountableForm, {
DiscountableFormType,
} from "../../forms/product/discountable-form"
@@ -33,6 +34,7 @@ type GeneralFormWrapper = {
}
const GeneralModal = ({ product, open, onClose }: Props) => {
const { t } = useTranslation()
const { onUpdate, updating } = useEditProductActions(product.id)
const form = useForm<GeneralFormWrapper>({
defaultValues: getDefaultValues(product),
@@ -95,7 +97,10 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
<Modal.Body>
<Modal.Header handleClose={onReset}>
<h1 className="inter-xlarge-semibold m-0">
Edit General Information
{t(
"product-general-section-edit-general-information",
"Edit General Information"
)}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
@@ -106,7 +111,10 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
/>
<div className="my-xlarge">
<h2 className="inter-base-semibold mb-base">
Organize {product.is_giftcard ? "Gift Card" : "Product"}
Organize{" "}
{product.is_giftcard
? t("product-general-section-gift-card", "Gift Card")
: t("product-general-section-product", "Product")}
</h2>
<OrganizeForm form={nestedForm(form, "organize")} />
</div>
@@ -115,7 +123,9 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
isGiftCard={product.is_giftcard}
/>
<div className="mt-xlarge">
<h2 className="inter-base-semibold mb-base">Metadata</h2>
<h2 className="inter-base-semibold mb-base">
{t("product-general-section-metadata", "Metadata")}
</h2>
<MetadataForm form={nestedForm(form, "metadata")} />
</div>
</Modal.Content>
@@ -127,7 +137,7 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
type="button"
onClick={onReset}
>
Cancel
{t("product-general-section-cancel", "Cancel")}
</Button>
<Button
size="small"
@@ -136,7 +146,7 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
disabled={!isDirty}
loading={updating}
>
Save
{t("product-general-section-save", "Save")}
</Button>
</div>
</Modal.Footer>

View File

@@ -1,4 +1,5 @@
import { Product } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useToggleState from "../../../hooks/use-toggle-state"
import {
@@ -22,6 +23,7 @@ type Props = {
}
const ProductGeneralSection = ({ product }: Props) => {
const { t } = useTranslation()
const { onDelete, onStatusChange } = useEditProductActions(product.id)
const {
state: infoState,
@@ -39,12 +41,15 @@ const ProductGeneralSection = ({ product }: Props) => {
const actions: ActionType[] = [
{
label: "Edit General Information",
label: t(
"product-general-section-edit-general-information",
"Edit General Information"
),
onClick: toggleInfo,
icon: <EditIcon size={20} />,
},
{
label: "Delete",
label: t("product-general-section-delete", "Delete"),
onClick: onDelete,
variant: "danger",
icon: <TrashIcon size={20} />,
@@ -53,7 +58,10 @@ const ProductGeneralSection = ({ product }: Props) => {
if (isFeatureEnabled("sales_channels")) {
actions.splice(1, 0, {
label: "Edit Sales Channels",
label: t(
"product-general-section-edit-sales-channels",
"Edit Sales Channels"
),
onClick: toggleChannels,
icon: <ChannelsIcon size={20} />,
})
@@ -68,8 +76,8 @@ const ProductGeneralSection = ({ product }: Props) => {
status={
<StatusSelector
isDraft={product?.status === "draft"}
activeState="Published"
draftState="Draft"
activeState={t("product-general-section-published", "Published")}
draftState={t("product-general-section-draft", "Draft")}
onChange={() => onStatusChange(product.status)}
/>
}
@@ -123,33 +131,50 @@ const Detail = ({ title, value }: DetailProps) => {
const ProductDetails = ({ product }: Props) => {
const { isFeatureEnabled } = useFeatureFlag()
const { t } = useTranslation()
return (
<div className="mt-8 flex flex-col gap-y-3">
<h2 className="inter-base-semibold">Details</h2>
<Detail title="Subtitle" value={product.subtitle} />
<Detail title="Handle" value={product.handle} />
<Detail title="Type" value={product.type?.value} />
<Detail title="Collection" value={product.collection?.title} />
<h2 className="inter-base-semibold">
{t("product-general-section-details", "Details")}
</h2>
<Detail
title={t("product-general-section-subtitle", "Subtitle")}
value={product.subtitle}
/>
<Detail
title={t("product-general-section-handle", "Handle")}
value={product.handle}
/>
<Detail
title={t("product-general-section-type", "Type")}
value={product.type?.value}
/>
<Detail
title={t("product-general-section-collection", "Collection")}
value={product.collection?.title}
/>
{isFeatureEnabled(FeatureFlag.PRODUCT_CATEGORIES) && (
<Detail
title="Category"
title={t("product-general-section-category", "Category")}
value={product.categories.map((c) => c.name)}
/>
)}
<Detail
title="Discountable"
value={product.discountable ? "True" : "False"}
title={t("product-general-section-discountable", "Discountable")}
value={
product.discountable
? t("product-general-section-true", "True")
: t("product-general-section-false", "False")
}
/>
<Detail
title="Metadata"
title={t("product-general-section-metadata", "Metadata")}
value={
Object.entries(product.metadata || {}).length > 0
? `${Object.entries(product.metadata || {}).length} ${
Object.keys(product.metadata || {}).length === 1
? "item"
: "items"
}`
? t("product-general-section-count", "{{count}}", {
count: Object.keys(product.metadata || {}).length,
})
: undefined
}
/>
@@ -176,10 +201,13 @@ const ProductTags = ({ product }: Props) => {
}
const ProductSalesChannels = ({ product }: Props) => {
const { t } = useTranslation()
return (
<FeatureToggle featureFlag="sales_channels">
<div className="mt-xlarge">
<h2 className="inter-base-semibold mb-xsmall">Sales channels</h2>
<h2 className="inter-base-semibold mb-xsmall">
{t("product-general-section-sales-channels", "Sales channels")}
</h2>
<SalesChannelsDisplay channels={product.sales_channels} />
</div>
</FeatureToggle>

View File

@@ -1,4 +1,5 @@
import { Product } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import useToggleState from "../../../hooks/use-toggle-state"
import { ActionType } from "../../molecules/actionables"
import Section from "../../organisms/section"
@@ -10,10 +11,11 @@ type Props = {
const ProductMediaSection = ({ product }: Props) => {
const { state, close, toggle } = useToggleState()
const { t } = useTranslation()
const actions: ActionType[] = [
{
label: "Edit Media",
label: t("product-media-section-edit-media", "Edit Media"),
onClick: toggle,
},
]

View File

@@ -1,6 +1,7 @@
import { Product } from "@medusajs/medusa"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useNotification from "../../../hooks/use-notification"
import { FormImage } from "../../../types/shared"
@@ -21,6 +22,7 @@ type MediaFormWrapper = {
}
const MediaModal = ({ product, open, onClose }: Props) => {
const { t } = useTranslation()
const { onUpdate, updating } = useEditProductActions(product.id)
const form = useForm<MediaFormWrapper>({
defaultValues: getDefaultValues(product),
@@ -49,17 +51,27 @@ const MediaModal = ({ product, open, onClose }: Props) => {
try {
preppedImages = await prepareImages(data.media.images)
} catch (error) {
let errorMessage = "Something went wrong while trying to upload images."
let errorMessage = t(
"product-media-section-upload-images-error",
"Something went wrong while trying to upload images."
)
const response = (error as any).response as Response
if (response.status === 500) {
errorMessage =
errorMessage +
" " +
"You might not have a file service configured. Please contact your administrator"
t(
"product-media-section-file-service-not-configured",
"You might not have a file service configured. Please contact your administrator"
)
}
notification("Error", errorMessage, "error")
notification(
t("product-media-section-error", "Error"),
errorMessage,
"error"
)
return
}
const urls = preppedImages.map((image) => image.url)
@@ -76,14 +88,21 @@ const MediaModal = ({ product, open, onClose }: Props) => {
<Modal open={open} handleClose={onReset} isLargeModal>
<Modal.Body>
<Modal.Header handleClose={onReset}>
<h1 className="inter-xlarge-semibold m-0">Edit Media</h1>
<h1 className="inter-xlarge-semibold m-0">
{t("product-media-section-edit-media", "Edit Media")}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
<div>
<h2 className="inter-large-semibold mb-2xsmall">Media</h2>
<h2 className="inter-large-semibold mb-2xsmall">
{t("product-media-section-media", "Media")}
</h2>
<p className="inter-base-regular text-grey-50 mb-large">
Add images to your product.
{t(
"product-media-section-add-images-to-your-product",
"Add images to your product."
)}
</p>
<div>
<MediaForm form={nestedForm(form, "media")} />
@@ -98,7 +117,7 @@ const MediaModal = ({ product, open, onClose }: Props) => {
type="button"
onClick={onReset}
>
Cancel
{t("product-media-section-cancel", "Cancel")}
</Button>
<Button
size="small"
@@ -107,7 +126,7 @@ const MediaModal = ({ product, open, onClose }: Props) => {
disabled={!isDirty}
loading={updating}
>
Save and close
{t("product-media-section-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>

View File

@@ -1,4 +1,5 @@
import { Product } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import JSONView from "../../molecules/json-view"
import Section from "../section"
@@ -8,8 +9,15 @@ type Props = {
/** Temporary component, should be replaced with <RawJson /> but since the design is different we will use this to not break the existing design across admin. */
const ProductRawSection = ({ product }: Props) => {
const { t } = useTranslation()
return (
<Section title={product.is_giftcard ? "Raw Gift Card" : "Raw Product"}>
<Section
title={
product.is_giftcard
? t("product-raw-section-raw-gift-card", "Raw Gift Card")
: t("product-raw-section-raw-product", "Raw Product")
}
>
<div className="pt-base">
<JSONView data={product} />
</div>

View File

@@ -1,5 +1,6 @@
import { Product } from "@medusajs/medusa"
import clsx from "clsx"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state"
@@ -14,6 +15,7 @@ type Props = {
}
const ProductThumbnailSection = ({ product }: Props) => {
const { t } = useTranslation()
const { onUpdate, updating } = useEditProductActions(product.id)
const { state, toggle, close } = useToggleState()
@@ -27,10 +29,21 @@ const ProductThumbnailSection = ({ product }: Props) => {
},
{
onSuccess: () => {
notification("Success", "Successfully deleted thumbnail", "success")
notification(
t("product-thumbnail-section-success", "Success"),
t(
"product-thumbnail-section-successfully-deleted-thumbnail",
"Successfully deleted thumbnail"
),
"success"
)
},
onError: (err) => {
notification("Error", getErrorMessage(err), "error")
notification(
t("product-thumbnail-section-error", "Error"),
getErrorMessage(err),
"error"
)
},
}
)
@@ -48,7 +61,9 @@ const ProductThumbnailSection = ({ product }: Props) => {
type="button"
onClick={toggle}
>
{product.thumbnail ? "Edit" : "Upload"}
{product.thumbnail
? t("product-thumbnail-section-edit", "Edit")
: t("product-thumbnail-section-upload", "Upload")}
</Button>
{product.thumbnail && (
<TwoStepDelete onDelete={handleDelete} deleting={updating} />

View File

@@ -1,6 +1,7 @@
import { Product } from "@medusajs/medusa"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useNotification from "../../../hooks/use-notification"
import { FormImage } from "../../../types/shared"
@@ -23,6 +24,7 @@ type ThumbnailFormWrapper = {
}
const ThumbnailModal = ({ product, open, onClose }: Props) => {
const { t } = useTranslation()
const { onUpdate, updating } = useEditProductActions(product.id)
const form = useForm<ThumbnailFormWrapper>({
defaultValues: getDefaultValues(product),
@@ -51,18 +53,27 @@ const ThumbnailModal = ({ product, open, onClose }: Props) => {
try {
preppedImages = await prepareImages(data.thumbnail.images)
} catch (error) {
let errorMessage =
let errorMessage = t(
"product-thumbnail-section-upload-thumbnail-error",
"Something went wrong while trying to upload the thumbnail."
)
const response = (error as any).response as Response
if (response.status === 500) {
errorMessage =
errorMessage +
" " +
"You might not have a file service configured. Please contact your administrator"
t(
"product-thumbnail-section-you-might-not-have-a-file-service-configured-please-contact-your-administrator",
"You might not have a file service configured. Please contact your administrator"
)
}
notification("Error", errorMessage, "error")
notification(
t("product-thumbnail-section-error", "Error"),
errorMessage,
"error"
)
return
}
const url = preppedImages?.[0]?.url
@@ -80,14 +91,23 @@ const ThumbnailModal = ({ product, open, onClose }: Props) => {
<Modal open={open} handleClose={onReset} isLargeModal>
<Modal.Body>
<Modal.Header handleClose={onReset}>
<h1 className="inter-xlarge-semibold m-0">Upload Thumbnail</h1>
<h1 className="inter-xlarge-semibold m-0">
{t(
"product-thumbnail-section-upload-thumbnail",
"Upload Thumbnail"
)}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
<h2 className="inter-large-semibold mb-2xsmall">Thumbnail</h2>
<h2 className="inter-large-semibold mb-2xsmall">
{t("product-thumbnail-section-thumbnail", "Thumbnail")}
</h2>
<p className="inter-base-regular text-grey-50 mb-large">
Used to represent your product during checkout, social sharing and
more.
{t(
"product-thumbnail-section-used-to-represent-your-product-during-checkout-social-sharing-and-more",
"Used to represent your product during checkout, social sharing and more."
)}
</p>
<ThumbnailForm form={nestedForm(form, "thumbnail")} />
</Modal.Content>
@@ -99,7 +119,7 @@ const ThumbnailModal = ({ product, open, onClose }: Props) => {
type="button"
onClick={onReset}
>
Cancel
{t("product-thumbnail-section-cancel", "Cancel")}
</Button>
<Button
size="small"
@@ -108,7 +128,10 @@ const ThumbnailModal = ({ product, open, onClose }: Props) => {
disabled={!isDirty}
loading={updating}
>
Save and close
{t(
"product-thumbnail-section-save-and-close",
"Save and close"
)}
</Button>
</div>
</Modal.Footer>

View File

@@ -1,5 +1,6 @@
import { Product, ProductVariant } from "@medusajs/medusa"
import React from "react"
import { useTranslation } from "react-i18next"
import { ActionType } from "../../molecules/actionables"
import { CollapsibleTree } from "../../molecules/collapsible-tree"
@@ -58,6 +59,7 @@ const ProductVariantTree: React.FC<ProductVariantTreeProps> = ({
}
const ProductVariantLeaf = ({ sku, title, prices = [] }: LeafProps) => {
const { t } = useTranslation()
const filteredPrices = prices.filter((pr) => pr.price_list_id)
return (
<div className="flex flex-1">
@@ -68,12 +70,14 @@ const ProductVariantLeaf = ({ sku, title, prices = [] }: LeafProps) => {
<div className="text-grey-50 flex flex-1 items-center justify-end">
<div className="text-grey-50 mr-xsmall">
{filteredPrices.length ? (
<span>{`${filteredPrices.length} price${
filteredPrices.length > 1 ? "s" : ""
}`}</span>
<span>
{t("product-variant-tree-count", "{{count}}", {
count: filteredPrices.length,
})}
</span>
) : (
<span className="inter-small-semibold text-orange-40">
Add prices
{t("product-variant-tree-add-prices", "Add prices")}
</span>
)}
</div>

View File

@@ -1,5 +1,6 @@
import { AdminPostProductsProductVariantsReq, Product } from "@medusajs/medusa"
import { useEffect } from "react"
import { useTranslation } from "react-i18next"
import EditFlowVariantForm, {
EditFlowVariantFormType,
} from "../../forms/product/variant-form/edit-flow-variant-form"
@@ -22,6 +23,7 @@ type Props = {
const AddVariantModal = ({ open, onClose, product }: Props) => {
const context = useLayeredModal()
const { t } = useTranslation()
const { client } = useMedusa()
const form = useForm<EditFlowVariantFormType>({
@@ -93,7 +95,9 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
<LayeredModal context={context} open={open} handleClose={resetAndClose}>
<Modal.Body>
<Modal.Header handleClose={resetAndClose}>
<h1 className="inter-xlarge-semibold">Add Variant</h1>
<h1 className="inter-xlarge-semibold">
{t("product-variants-section-add-variant", "Add Variant")}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
@@ -107,7 +111,7 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
type="button"
onClick={resetAndClose}
>
Cancel
{t("product-variants-section-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -115,7 +119,7 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
type="submit"
loading={addingVariant}
>
Save and close
{t("product-variants-section-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>

View File

@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next"
import EditFlowVariantForm, {
EditFlowVariantFormType,
} from "../../forms/product/variant-inventory-form/edit-flow-variant-form"
@@ -30,6 +31,7 @@ type Props = {
}
const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
const { t } = useTranslation()
const { client } = useMedusa()
const layeredModalContext = useContext(LayeredModalContext)
const {
@@ -191,7 +193,12 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
return (
<LayeredModal context={layeredModalContext} handleClose={handleClose}>
<Modal.Header handleClose={handleClose}>
<h1 className="inter-xlarge-semibold">Edit stock & inventory</h1>
<h1 className="inter-xlarge-semibold">
{t(
"product-variants-section-edit-stock-inventory",
"Edit stock & inventory"
)}
</h1>
</Modal.Header>
{!isLoadingInventory && (
<StockForm
@@ -222,6 +229,7 @@ const StockForm = ({
handleClose: () => void
updatingVariant: boolean
}) => {
const { t } = useTranslation()
const form = useForm<EditFlowVariantFormType>({
// @ts-ignore
defaultValues: getEditVariantDefaultValues(variantInventory, variant),
@@ -259,7 +267,7 @@ const StockForm = ({
handleClose()
}}
>
Cancel
{t("product-variants-section-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -268,7 +276,7 @@ const StockForm = ({
disabled={!isDirty}
loading={updatingVariant}
>
Save and close
{t("product-variants-section-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>

View File

@@ -1,4 +1,5 @@
import { Product, ProductVariant } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import EditFlowVariantForm, {
EditFlowVariantFormType,
} from "../../forms/product/variant-form/edit-flow-variant-form"
@@ -30,6 +31,7 @@ const EditVariantModal = ({
variant,
isDuplicate = false,
}: Props) => {
const { t } = useTranslation()
const form = useForm<EditFlowVariantFormType>({
// @ts-ignore
defaultValues: getEditVariantDefaultValues(variant, product),
@@ -111,7 +113,7 @@ const EditVariantModal = ({
<LayeredModal context={layeredModalContext} handleClose={handleClose}>
<Modal.Header handleClose={handleClose}>
<h1 className="inter-xlarge-semibold">
Edit Variant
{t("product-variants-section-edit-variant", "Edit Variant")}
{variant.title && (
<span className="inter-xlarge-regular text-grey-50">
{" "}
@@ -132,7 +134,7 @@ const EditVariantModal = ({
type="button"
onClick={handleClose}
>
Cancel
{t("product-variants-section-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -141,7 +143,7 @@ const EditVariantModal = ({
disabled={!isDirty && !isDuplicate}
loading={addingVariant || updatingVariant}
>
Save and close
{t("product-variants-section-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>

View File

@@ -4,6 +4,7 @@ import {
ProductVariant,
} from "@medusajs/medusa"
import React, { useContext, useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import EditFlowVariantForm, {
EditFlowVariantFormType,
} from "../../../forms/product/variant-form/edit-flow-variant-form"
@@ -23,6 +24,7 @@ type Props = {
}
const EditVariantScreen = ({ variant, product }: Props) => {
const { t } = useTranslation()
const { onClose } = useEditVariantsModal()
const form = useForm<EditFlowVariantFormType>({
defaultValues: getEditVariantDefaultValues(variant, product),
@@ -67,7 +69,7 @@ const EditVariantScreen = ({ variant, product }: Props) => {
<Modal.Footer>
<div className="gap-x-xsmall flex w-full items-center justify-end">
<Button variant="secondary" size="small" type="button">
Cancel
{t("edit-variants-modal-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -77,7 +79,7 @@ const EditVariantScreen = ({ variant, product }: Props) => {
loading={updatingVariant}
onClick={onSubmitAndBack}
>
Save and go back
{t("edit-variants-modal-save-and-go-back", "Save and go back")}
</Button>
<Button
variant="primary"
@@ -87,7 +89,7 @@ const EditVariantScreen = ({ variant, product }: Props) => {
loading={updatingVariant}
onClick={onSubmitAndClose}
>
Save and close
{t("edit-variants-modal-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>
@@ -133,11 +135,12 @@ export const createUpdatePayload = (
}
export const useEditVariantScreen = (props: Props) => {
const { t } = useTranslation()
const { pop } = React.useContext(LayeredModalContext)
const screen = useMemo(() => {
return {
title: "Edit Variant",
title: t("edit-variants-modal-edit-variant", "Edit Variant"),
subtitle: props.variant.title,
onBack: pop,
view: <EditVariantScreen {...props} />,

View File

@@ -6,6 +6,7 @@ import {
useFieldArray,
useForm,
} from "react-hook-form"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../../hooks/use-edit-product-actions"
import Button from "../../../fundamentals/button"
import Modal from "../../../molecules/modal"
@@ -36,6 +37,7 @@ export type EditVariantsForm = {
const EditVariantsModal = ({ open, onClose, product }: Props) => {
const context = useContext(LayeredModalContext)
const { onUpdate, updating } = useEditProductActions(product.id)
const { t } = useTranslation()
const form = useForm<EditVariantsForm>({
defaultValues: getDefaultValues(product),
@@ -109,7 +111,10 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
() => {
resetAndClose()
},
"Variants were successfully updated"
t(
"edit-variants-modal-update-success",
"Variants were successfully updated"
)
)
})
@@ -122,20 +127,29 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
<LayeredModal handleClose={resetAndClose} open={open} context={context}>
<Modal.Body>
<Modal.Header handleClose={resetAndClose}>
<h1 className="inter-xlarge-semibold">Edit Variants</h1>
<h1 className="inter-xlarge-semibold">
{t("edit-variants-modal-edit-variants", "Edit Variants")}
</h1>
</Modal.Header>
<FormProvider {...form}>
<form onSubmit={onSubmit}>
<Modal.Content>
<h2 className="inter-base-semibold mb-small">
Product variants{" "}
{t(
"edit-variants-modal-product-variants",
"Product variants"
)}{" "}
<span className="inter-base-regular text-grey-50">
({product.variants.length})
</span>
</h2>
<div className="pr-base inter-small-semibold text-grey-50 mb-small grid grid-cols-[1fr_1fr_48px]">
<p className="col-start-1 col-end-1 text-left">Variant</p>
<p className="col-start-2 col-end-2 text-right">Inventory</p>
<p className="col-start-1 col-end-1 text-left">
{t("edit-variants-modal-variant", "Variant")}
</p>
<p className="col-start-2 col-end-2 text-right">
{t("edit-variants-modal-inventory", "Inventory")}
</p>
</div>
<div>{fields.map((card, i) => renderCard(card, i))}</div>
</Modal.Content>
@@ -147,7 +161,7 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
type="button"
onClick={resetAndClose}
>
Cancel
{t("edit-variants-modal-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -156,7 +170,7 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
loading={updating}
disabled={updating || !isDirty}
>
Save and close
{t("edit-variants-modal-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>

View File

@@ -4,6 +4,7 @@ import type { Identifier, XYCoord } from "dnd-core"
import { useContext, useMemo, useRef } from "react"
import { useDrag, useDrop } from "react-dnd"
import { useFormContext } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { VariantItem } from "."
import { DragItem } from "../../../../types/shared"
import FormValidator from "../../../../utils/form-validator"
@@ -35,6 +36,7 @@ export const VariantCard = ({
moveCard,
product,
}: VariantCardProps) => {
const { t } = useTranslation()
const {
register,
formState: { errors },
@@ -49,7 +51,7 @@ export const VariantCard = ({
const actions: ActionType[] = useMemo(() => {
return [
{
label: "Edit Variant",
label: t("edit-variants-modal-edit-variant", "Edit Variant"),
icon: <EditIcon size={20} className="text-grey-50" />,
onClick: () => push(editVariantScreen),
},

View File

@@ -1,5 +1,6 @@
import OptionsProvider, { useOptionsContext } from "./options-provider"
import { Product, ProductVariant, VariantInventory } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import { ActionType } from "../../molecules/actionables"
import AddVariantModal from "./add-variant-modal"
@@ -28,6 +29,7 @@ type Props = {
const ProductVariantsSection = ({ product }: Props) => {
const queryClient = useQueryClient()
const { client } = useMedusa()
const { t } = useTranslation()
const { isFeatureEnabled } = useFeatureFlag()
@@ -69,22 +71,22 @@ const ProductVariantsSection = ({ product }: Props) => {
const actions: ActionType[] = [
{
label: "Add Variant",
label: t("product-variants-section-add-variant", "Add Variant"),
onClick: toggleAddVariant,
icon: <PlusIcon size="20" />,
},
{
label: "Edit Prices",
label: t("product-variants-section-edit-prices", "Edit Prices"),
onClick: toggleEditPrices,
icon: <DollarSignIcon size="20" />,
},
{
label: "Edit Variants",
label: t("product-variants-section-edit-variants", "Edit Variants"),
onClick: toggleEditVariants,
icon: <EditIcon size="20" />,
},
{
label: "Edit Options",
label: t("product-variants-section-edit-options", "Edit Options"),
onClick: toggleOptions,
icon: <GearIcon size="20" />,
},
@@ -130,7 +132,7 @@ const ProductVariantsSection = ({ product }: Props) => {
<ProductOptions />
<div className="mt-xlarge">
<h2 className="inter-large-semibold mb-base">
Product variants{" "}
{t("product-variants-section-product-variants", "Product variants")}{" "}
<span className="inter-large-regular text-grey-50">
({product.variants.length})
</span>

View File

@@ -6,6 +6,7 @@ import {
} from "medusa-react"
import { useEffect, useMemo } from "react"
import { useFieldArray, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification"
import FormValidator from "../../../utils/form-validator"
import Button from "../../fundamentals/button"
@@ -41,6 +42,7 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
product.id
)
const { t } = useTranslation()
const { refetch } = useOptionsContext()
const {
@@ -140,7 +142,14 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
})
if (errors.length === toCreate.length + toUpdate.length + toDelete.length) {
notification("Error", "Failed to update product options", "error")
notification(
t("product-variants-section-error", "Error"),
t(
"product-variants-section-failed-to-update-product-options",
"Failed to update product options"
),
"error"
)
return
}
@@ -153,7 +162,14 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
}
refetch()
notification("Success", "Successfully updated product options", "success")
notification(
t("product-variants-section-success", "Success"),
t(
"product-variants-section-successfully-updated-product-options",
"Successfully updated product options"
),
"success"
)
handleClose()
})
@@ -161,13 +177,19 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
<Modal open={open} handleClose={handleClose}>
<Modal.Body>
<Modal.Header handleClose={handleClose}>
<h1 className="inter-xlarge-semibold">Edit Options</h1>
<h1 className="inter-xlarge-semibold">
{t("product-variants-section-edit-options", "Edit Options")}
</h1>
</Modal.Header>
<form onSubmit={onSubmit}>
<Modal.Content>
<h2 className="inter-large-semibold mb-base">Product options</h2>
<h2 className="inter-large-semibold mb-base">
{t("product-variants-section-product-options", "Product options")}
</h2>
<div className="gap-y-small flex flex-col">
<p className="inter-small-semibold text-grey-50">Option title</p>
<p className="inter-small-semibold text-grey-50">
{t("product-variants-section-option-title", "Option title")}
</p>
<div className="gap-y-xsmall flex flex-col">
{fields.map((field, index) => {
return (
@@ -179,7 +201,10 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
key={field.id}
placeholder="Color"
{...register(`options.${index}.title`, {
required: "Option title is required",
required: t(
"product-variants-section-option-title-is-required",
"Option title is required"
),
minLength:
FormValidator.minOneCharRule("Option title"),
pattern: FormValidator.whiteSpaceRule("Option title"),
@@ -205,7 +230,8 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
type="button"
onClick={handleAddAnOption}
>
<PlusIcon size="20" /> Add an option
<PlusIcon size="20" />{" "}
{t("product-variants-section-add-an-option", "Add an option")}
</Button>
</Modal.Content>
<Modal.Footer>
@@ -216,7 +242,7 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
type="button"
onClick={handleClose}
>
Cancel
{t("product-variants-section-cancel", "Cancel")}
</Button>
<Button
variant="primary"
@@ -225,7 +251,7 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
disabled={!isDirty}
loading={isSubmitting}
>
Save and close
{t("product-variants-section-save-and-close", "Save and close")}
</Button>
</div>
</Modal.Footer>

View File

@@ -2,6 +2,7 @@ import { Column, useTable } from "react-table"
import { ProductVariant } from "@medusajs/medusa"
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
import BuildingsIcon from "../../fundamentals/icons/buildings-icon"
import DuplicateIcon from "../../fundamentals/icons/duplicate-icon"
@@ -22,6 +23,7 @@ type Props = {
}
export const useVariantsTableColumns = (inventoryIsEnabled = false) => {
const { t } = useTranslation()
const columns = useMemo<Column<ProductVariant>[]>(() => {
const quantityColumns = []
if (!inventoryIsEnabled) {
@@ -29,7 +31,9 @@ export const useVariantsTableColumns = (inventoryIsEnabled = false) => {
Header: () => {
return (
<div className="text-right">
<span>Inventory</span>
<span>
{t("product-variants-section-inventory", "Inventory")}
</span>
</div>
)
},
@@ -47,12 +51,12 @@ export const useVariantsTableColumns = (inventoryIsEnabled = false) => {
}
return [
{
Header: "Title",
Header: t("product-variants-section-title", "Title"),
id: "title",
accessor: "title",
},
{
Header: "SKU",
Header: t("product-variants-section-sku", "SKU"),
id: "sku",
accessor: "sku",
maxWidth: 264,
@@ -65,7 +69,7 @@ export const useVariantsTableColumns = (inventoryIsEnabled = false) => {
},
},
{
Header: "EAN",
Header: t("product-variants-section-ean", "EAN"),
id: "ean",
accessor: "ean",
maxWidth: 264,
@@ -85,6 +89,7 @@ export const useVariantsTableColumns = (inventoryIsEnabled = false) => {
}
const VariantsTable = ({ variants, actions }: Props) => {
const { t } = useTranslation()
const { isFeatureEnabled } = useFeatureFlag()
const hasInventoryService = isFeatureEnabled("inventoryService")
const columns = useVariantsTableColumns(hasInventoryService)
@@ -112,20 +117,26 @@ const VariantsTable = ({ variants, actions }: Props) => {
const inventoryManagementActions = []
if (hasInventoryService) {
inventoryManagementActions.push({
label: "Manage inventory",
label: t(
"product-variants-section-manage-inventory",
"Manage inventory"
),
icon: <BuildingsIcon size="20" />,
onClick: () => updateVariantInventory(variant),
})
}
return [
{
label: "Edit Variant",
label: t("product-variants-section-edit-variant", "Edit Variant"),
icon: <EditIcon size="20" />,
onClick: () => updateVariant(variant),
},
...inventoryManagementActions,
{
label: "Duplicate Variant",
label: t(
"product-variants-section-duplicate-variant",
"Duplicate Variant"
),
onClick: () =>
// @ts-ignore
duplicateVariant({
@@ -135,7 +146,10 @@ const VariantsTable = ({ variants, actions }: Props) => {
icon: <DuplicateIcon size="20" />,
},
{
label: "Delete Variant",
label: t(
"product-variants-section-delete-variant-label",
"Delete Variant"
),
onClick: () => setVariantToRemove(variant),
icon: <TrashIcon size="20" />,
variant: "danger",
@@ -185,11 +199,23 @@ const VariantsTable = ({ variants, actions }: Props) => {
<DeletePrompt
onDelete={async () => deleteVariant(variantToRemove.id)}
handleClose={() => setVariantToRemove(null)}
confirmText="Yes, delete"
heading="Delete variant"
text={`Are you sure you want to delete this variant? ${
confirmText={t(
"product-variants-section-yes-delete",
"Yes, delete"
)}
heading={t(
"product-variants-section-delete-variant-heading",
"Delete variant"
)}
text={`${t(
"product-variants-section-confirm-delete",
"Are you sure you want to delete this variant? "
)}${
isFeatureEnabled("inventoryService")
? " Note: Deleting the variant will also remove inventory items and levels"
? t(
"product-variants-section-note-deleting-the-variant-will-also-remove-inventory-items-and-levels",
" Note: Deleting the variant will also remove inventory items and levels"
)
: ""
}`}
successText={false}

View File

@@ -1,6 +1,7 @@
import { useAdminSendResetPasswordToken } from "medusa-react"
import React, { useState } from "react"
import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages"
import FormValidator from "../../../utils/form-validator"
@@ -22,6 +23,7 @@ const emailRegex = new RegExp(
)
const ResetTokenCard: React.FC<ResetTokenCardProps> = ({ goBack }) => {
const { t } = useTranslation()
const [mailSent, setSentMail] = useState(false)
const {
register,
@@ -42,7 +44,11 @@ const ResetTokenCard: React.FC<ResetTokenCardProps> = ({ goBack }) => {
setSentMail(true)
},
onError: (error) => {
notification("Error", getErrorMessage(error), "error")
notification(
t("reset-token-card-error", "Error"),
getErrorMessage(error),
"error"
)
},
}
)
@@ -52,25 +58,30 @@ const ResetTokenCard: React.FC<ResetTokenCardProps> = ({ goBack }) => {
<form onSubmit={onSubmit}>
<div className="flex flex-col items-center">
<h1 className="inter-xlarge-semibold text-grey-90 mb-xsmall text-[20px]">
Reset your password
{t("reset-token-card-reset-your-password", "Reset your password")}
</h1>
<span className="inter-base-regular text-grey-50 mb-large text-center">
Enter your email address below, and we&apos;ll
<br />
send you instructions on how to reset
<br />
your password.
<Trans t={t} i18nKey="reset-token-card-password-reset-description">
Enter your email address below, and we'll
<br />
send you instructions on how to reset
<br />
your password.
</Trans>
</span>
{!mailSent ? (
<>
<div className="w-[280px]">
<SigninInput
placeholder="Email"
placeholder={t("reset-token-card-email", "Email")}
{...register("email", {
required: FormValidator.required("Email"),
pattern: {
value: emailRegex,
message: "This is not a valid email",
message: t(
"reset-token-card-this-is-not-a-valid-email",
"This is not a valid email"
),
},
})}
/>
@@ -83,7 +94,10 @@ const ResetTokenCard: React.FC<ResetTokenCardProps> = ({ goBack }) => {
type="submit"
loading={isLoading}
>
Send reset instructions
{t(
"reset-token-card-send-reset-instructions",
"Send reset instructions"
)}
</Button>
</>
) : (
@@ -93,7 +107,10 @@ const ResetTokenCard: React.FC<ResetTokenCardProps> = ({ goBack }) => {
</div>
<div className="gap-y-2xsmall flex flex-col">
<span className="inter-base-regular">
Successfully sent you an email
{t(
"reset-token-card-successfully-sent-you-an-email",
"Successfully sent you an email"
)}
</span>
</div>
</div>
@@ -102,7 +119,7 @@ const ResetTokenCard: React.FC<ResetTokenCardProps> = ({ goBack }) => {
className="inter-small-regular text-grey-50 mt-8 cursor-pointer"
onClick={goBack}
>
Go back to sign in
{t("reset-token-card-go-back-to-sign-in", "Go back to sign in")}
</span>
</div>
</form>

View File

@@ -1,5 +1,6 @@
import clsx from "clsx"
import React from "react"
import { useTranslation } from "react-i18next"
import { formatAmountWithSymbol } from "../../../utils/prices"
import Button from "../../fundamentals/button"
import MinusIcon from "../../fundamentals/icons/minus-icon"
@@ -39,11 +40,16 @@ const RMAReturnProductsTable: React.FC<RMAReturnProductsTableProps> = ({
handleRemoveItem,
handleToAddQuantity,
}) => {
const { t } = useTranslation()
return (
<Table>
<Table.HeadRow className="text-grey-50 inter-small-semibold">
<Table.HeadCell>Product Details</Table.HeadCell>
<Table.HeadCell className="pr-8 text-right">Quantity</Table.HeadCell>
<Table.HeadCell>
{t("rma-return-product-table-product-details", "Product Details")}
</Table.HeadCell>
<Table.HeadCell className="pr-8 text-right">
{t("rma-return-product-table-quantity", "Quantity")}
</Table.HeadCell>
<Table.HeadCell className="text-right">
{isAdditionalItems ? "Unit Price" : "Refundable"}
</Table.HeadCell>

View File

@@ -1,6 +1,7 @@
import { LineItem, Order } from "@medusajs/medusa"
import clsx from "clsx"
import React, { Fragment, useContext } from "react"
import { useTranslation } from "react-i18next"
import RMAReturnReasonSubModal from "../../../domain/orders/details/rma-sub-modals/return-reasons"
import Medusa from "../../../services/api"
import { isLineItemCanceled } from "../../../utils/is-line-item"
@@ -32,6 +33,7 @@ const RMASelectProductTable: React.FC<RMASelectProductTableProps> = ({
setToReturn,
isSwapOrClaim = false,
}) => {
const { t } = useTranslation()
const { push, pop } = useContext(LayeredModalContext)
const handleQuantity = (change, item) => {
@@ -112,9 +114,15 @@ const RMASelectProductTable: React.FC<RMASelectProductTableProps> = ({
<Table>
<Table.Head className="border-none">
<Table.HeadRow className="text-grey-50 inter-small-semibold">
<Table.HeadCell colSpan={2}>Product Details</Table.HeadCell>
<Table.HeadCell className="pr-8 text-right">Quantity</Table.HeadCell>
<Table.HeadCell className="text-right">Refundable</Table.HeadCell>
<Table.HeadCell colSpan={2}>
{t("rma-select-product-table-product-details", "Product Details")}
</Table.HeadCell>
<Table.HeadCell className="pr-8 text-right">
{t("rma-select-product-table-quantity", "Quantity")}
</Table.HeadCell>
<Table.HeadCell className="text-right">
{t("rma-select-product-table-refundable", "Refundable")}
</Table.HeadCell>
<Table.HeadCell></Table.HeadCell>
</Table.HeadRow>
</Table.Head>
@@ -231,11 +239,13 @@ const RMASelectProductTable: React.FC<RMASelectProductTableProps> = ({
<span className="ml-2">
{toReturn[item.id]?.images?.length > 0 && (
<>
({toReturn[item.id]?.images?.length} image{" "}
{toReturn[item.id]?.images?.length > 1
? "s"
: ""}
)
{t(
"rma-select-product-table-images-witch-count",
"{{count}}",
{
count: toReturn[item.id]?.images?.length,
}
)}
</>
)}
</span>
@@ -263,7 +273,10 @@ const RMASelectProductTable: React.FC<RMASelectProductTableProps> = ({
size="small"
className="border-grey-20 border"
>
Select Reason
{t(
"rma-select-product-table-select-reason",
"Select Reason"
)}
</Button>
</div>
</Table.Cell>

View File

@@ -1,5 +1,6 @@
import { useAdminStore } from "medusa-react"
import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
import { useRoutes } from "../../../providers/route-provider"
@@ -19,6 +20,7 @@ import UserMenu from "../../molecules/user-menu"
const ICON_SIZE = 20
const Sidebar: React.FC = () => {
const { t } = useTranslation()
const [currentlyOpen, setCurrentlyOpen] = useState(-1)
const { isFeatureEnabled } = useFeatureFlag()
@@ -50,7 +52,9 @@ const Sidebar: React.FC = () => {
</div>
</div>
<div className="my-base flex flex-col px-2">
<span className="text-grey-50 text-small font-medium">Store</span>
<span className="text-grey-50 text-small font-medium">
{t("sidebar-store", "Store")}
</span>
<span className="text-grey-90 text-medium font-medium">
{store?.name}
</span>
@@ -60,19 +64,19 @@ const Sidebar: React.FC = () => {
pageLink={"/a/orders"}
icon={<CartIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler}
text={"Orders"}
text={t("sidebar-orders", "Orders")}
/>
<SidebarMenuItem
pageLink={"/a/products"}
icon={<TagIcon size={ICON_SIZE} />}
text={"Products"}
text={t("sidebar-products", "Products")}
triggerHandler={triggerHandler}
/>
{isFeatureEnabled("product_categories") && (
<SidebarMenuItem
pageLink={"/a/product-categories"}
icon={<SwatchIcon size={ICON_SIZE} />}
text={"Categories"}
text={t("sidebar-categories", "Categories")}
triggerHandler={triggerHandler}
/>
)}
@@ -80,33 +84,33 @@ const Sidebar: React.FC = () => {
pageLink={"/a/customers"}
icon={<UsersIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler}
text={"Customers"}
text={t("sidebar-customers", "Customers")}
/>
{inventoryEnabled && (
<SidebarMenuItem
pageLink={"/a/inventory"}
icon={<BuildingsIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler}
text={"Inventory"}
text={t("sidebar-inventory", "Inventory")}
/>
)}
<SidebarMenuItem
pageLink={"/a/discounts"}
icon={<SaleIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler}
text={"Discounts"}
text={t("sidebar-discounts", "Discounts")}
/>
<SidebarMenuItem
pageLink={"/a/gift-cards"}
icon={<GiftIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler}
text={"Gift Cards"}
text={t("sidebar-gift-cards", "Gift Cards")}
/>
<SidebarMenuItem
pageLink={"/a/pricing"}
icon={<CashIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler}
text={"Pricing"}
text={t("sidebar-pricing", "Pricing")}
/>
{getLinks().map(({ path, label, icon }, index) => {
const cleanLink = path.replace("/a/", "")
@@ -127,7 +131,7 @@ const Sidebar: React.FC = () => {
pageLink={"/a/settings"}
icon={<GearIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler}
text={"Settings"}
text={t("sidebar-settings", "Settings")}
/>
</div>
</div>

View File

@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next"
import { SkeletonProvider } from "../../../providers/skeleton-provider"
import Skeleton from "../../atoms/skeleton"
import ArrowLeftIcon from "../../fundamentals/icons/arrow-left-icon"
@@ -24,6 +25,7 @@ export const TablePagination = ({
hasPrev,
},
}: Props) => {
const { t } = useTranslation()
const soothedOffset = count > 0 ? offset + 1 : 0
const soothedPageCount = Math.max(1, pageCount)
@@ -35,11 +37,31 @@ export const TablePagination = ({
}
>
<Skeleton>
<div>{`${soothedOffset} - ${pageSize} of ${count} ${title}`}</div>
<div>
{t(
"table-container-soothed-offset",
"{{soothedOffset}} - {{pageSize}} of {{count}} {{title}}",
{
soothedOffset,
pageSize,
count,
title,
}
)}
</div>
</Skeleton>
<div className="flex space-x-4">
<Skeleton>
<div>{`${currentPage} of ${soothedPageCount}`}</div>
<div>
{t(
"table-container-current-page",
"{{currentPage}} of {{soothedPageCount}}",
{
currentPage,
soothedPageCount,
}
)}
</div>
</Skeleton>
<div className="flex items-center space-x-4">
<button

View File

@@ -1,6 +1,7 @@
import clsx from "clsx"
import { useAdminCreateNote, useAdminOrder } from "medusa-react"
import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import RegisterClaimMenu from "../../../domain/orders/details/claim/register-claim-menu"
import ReturnMenu from "../../../domain/orders/details/returns"
@@ -55,6 +56,7 @@ type TimelineProps = {
}
const Timeline: React.FC<TimelineProps> = ({ orderId }) => {
const { t } = useTranslation()
const { orderRelations } = useOrdersExpandParam()
const { events, refetch } = useBuildTimeline(orderId)
@@ -76,17 +78,17 @@ const Timeline: React.FC<TimelineProps> = ({ orderId }) => {
const actions: ActionType[] = [
{
icon: <BackIcon size={20} />,
label: "Request Return",
label: t("timeline-request-return", "Request Return"),
onClick: () => setShowRequestReturn(true),
},
{
icon: <RefreshIcon size={20} />,
label: "Register Exchange",
label: t("timeline-register-exchange", "Register Exchange"),
onClick: () => setshowCreateSwap(true),
},
{
icon: <AlertIcon size={20} />,
label: "Register Claim",
label: t("timeline-register-claim", "Register Claim"),
onClick: openRegisterClaim,
},
]
@@ -102,8 +104,18 @@ const Timeline: React.FC<TimelineProps> = ({ orderId }) => {
value: value,
},
{
onSuccess: () => notification("Success", "Added note", "success"),
onError: (err) => notification("Error", getErrorMessage(err), "error"),
onSuccess: () =>
notification(
t("timeline-success", "Success"),
t("timeline-added-note", "Added note"),
"success"
),
onError: (err) =>
notification(
t("timeline-error", "Error"),
getErrorMessage(err),
"error"
),
}
)
}
@@ -113,7 +125,9 @@ const Timeline: React.FC<TimelineProps> = ({ orderId }) => {
<div className="rounded-rounded border-grey-20 bg-grey-0 h-full w-5/12 border">
<div className="border-grey-20 py-large px-xlarge border-b">
<div className="flex items-center justify-between">
<h3 className="inter-xlarge-semibold">Timeline</h3>
<h3 className="inter-xlarge-semibold">
{t("timeline-timeline", "Timeline")}
</h3>
<div
className={clsx({
"pointer-events-none opacity-50": !events,

View File

@@ -7,6 +7,7 @@ import NotificationBell from "../../molecules/notification-bell"
import SearchBar from "../../molecules/search-bar"
import ActivityDrawer from "../activity-drawer"
import MailDialog from "../help-dialog"
import LanguageMenu from "../../molecules/language-menu"
const Topbar: React.FC = () => {
const {

View File

@@ -1,6 +1,7 @@
import clsx from "clsx"
import { ReactNode, useState } from "react"
import { useHref } from "react-router-dom"
import { useTranslation } from "react-i18next"
import Tooltip from "../../atoms/tooltip"
import Button from "../../fundamentals/button"
@@ -83,16 +84,18 @@ type UploadSummaryProps = {
*/
function UploadSummary(props: UploadSummaryProps) {
const { creations, updates, type } = props
const { t } = useTranslation()
return (
<div className="flex gap-6">
<div className="text-small text-grey-90 flex items-center">
<CheckCircleIcon color="#9CA3AF" className="mr-2" />
<span className="font-semibold"> {creations || 0}&nbsp;</span> new{" "}
{type}
<span className="font-semibold"> {creations || 0}&nbsp;</span>{" "}
{t("upload-modal-new", "new")} {type}
</div>
<div className="text-small text-grey-90 flex items-center">
<WarningCircleIcon fill="#9CA3AF" className="mr-2" />
<span className="font-semibold">{updates || 0}&nbsp;</span> updates
<span className="font-semibold">{updates || 0}&nbsp;</span>{" "}
{t("upload-modal-updates", "updates")}
</div>
</div>
)
@@ -106,6 +109,7 @@ type DropAreaProps = {
* Component handles an CSV file drop.
*/
function DropArea(props: DropAreaProps) {
const { t } = useTranslation()
const [isDragOver, setIsDragOver] = useState(false)
const handleFileDrop = (e) => {
@@ -139,11 +143,11 @@ function DropArea(props: DropAreaProps) {
)}
>
<span className="text-grey-50 text-small">
Drop your file here, or
{t("upload-modal-drop-your-file-here-or", "Drop your file here, or")}
<a className="text-violet-60">
<label className="cursor-pointer" htmlFor="upload-form-file">
{" "}
click to browse.
{t("upload-modal-click-to-browse", "click to browse.")}
</label>
<input
type="file"
@@ -156,7 +160,10 @@ function DropArea(props: DropAreaProps) {
</a>
</span>
<span className="text-grey-40 text-small">
Only .csv files are supported.
{t(
"upload-modal-only-csv-files-are-supported",
"Only .csv files are supported."
)}
</span>
</div>
)
@@ -203,6 +210,7 @@ function UploadModal(props: UploadModalProps) {
type,
} = props
const [uploadFile, setUploadFile] = useState<File>()
const { t } = useTranslation()
const { name, size } = uploadFile || {}
@@ -222,7 +230,7 @@ function UploadModal(props: UploadModalProps) {
<Modal.Content>
<div className="flex justify-between">
<span className="text-grey-90 inter-large-semibold py-4 text-2xl">
Import {fileTitle}
Import {{ fileTitle }}
</span>
<button onClick={onClose} className="text-grey-50 cursor-pointer">
<CrossIcon size={20} />
@@ -230,7 +238,9 @@ function UploadModal(props: UploadModalProps) {
</div>
<div className="text-grey-90 inter-large-semibold mb-1 text-base">
Import {fileTitle}
{t("upload-modal-import-file-title", "Import {{fileTitle}}", {
fileTitle,
})}
</div>
<p className="text-grey-50 mb-4 text-base">{description1Text}</p>
@@ -293,7 +303,7 @@ function UploadModal(props: UploadModalProps) {
size="small"
onClick={onClose}
>
Cancel
{t("upload-modal-cancel", "Cancel")}
</Button>
<Button
@@ -303,7 +313,7 @@ function UploadModal(props: UploadModalProps) {
className="text-small"
onClick={onSubmit}
>
Import List
{t("upload-modal-import-list", "Import List")}
</Button>
</div>
</div>