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

This commit is contained in:
Geoffroy Empain authored and GitHub committed 2023-09-12 14:53:48 +02:00
1 parent 107aaa371c
commit afd4e72cdf
347 files changed
+9598 -2228

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/admin-ui": minor
"@medusajs/admin": minor
---
feat(admin-ui): Multi-language support
+7 -1
View File
@@ -28,7 +28,8 @@
"dev": "yarn create:dev:entry && webpack serve --mode=development --config ./webpack.config.dev.ts --progress profile", "dev": "yarn create:dev:entry && webpack serve --mode=development --config ./webpack.config.dev.ts --progress profile",
"analyze:bundle": "ANALYZE_BUNDLE=true webpack --config ./webpack.config.dev.ts", "analyze:bundle": "ANALYZE_BUNDLE=true webpack --config ./webpack.config.dev.ts",
"analyze:deps": "ANALYZE_DEPS=true webpack serve --config ./webpack.config.dev.ts --progress profile", "analyze:deps": "ANALYZE_DEPS=true webpack serve --config ./webpack.config.dev.ts --progress profile",
"build": "tsup" "build": "tsup",
"sync:i18n": "i18next 'ui/src/**/*.{ts,tsx,js,jsx}' -c 'ui/i18next-parser.config.js'"
}, },
"dependencies": { "dependencies": {
"@babel/parser": "7.22.5", "@babel/parser": "7.22.5",
@@ -62,6 +63,9 @@
"emoji-picker-react": "^4.4.3", "emoji-picker-react": "^4.4.3",
"framer-motion": "^9.1.6", "framer-motion": "^9.1.6",
"html-webpack-plugin": "^5.5.1", "html-webpack-plugin": "^5.5.1",
"i18next": "^22.5.0",
"i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.2.1",
"md5": "^2.3.0", "md5": "^2.3.0",
"medusa-react": "*", "medusa-react": "*",
"mini-css-extract-plugin": "^2.7.6", "mini-css-extract-plugin": "^2.7.6",
@@ -88,6 +92,7 @@
"react-hook-form": "7.38.0", "react-hook-form": "7.38.0",
"react-hot-toast": "2.4.0", "react-hot-toast": "2.4.0",
"react-hotkeys-hook": "^3.4.7", "react-hotkeys-hook": "^3.4.7",
"react-i18next": "^12.3.1",
"react-json-tree": "^0.17.0", "react-json-tree": "^0.17.0",
"react-jwt": "^1.1.4", "react-jwt": "^1.1.4",
"react-nestable": "^2.0.0", "react-nestable": "^2.0.0",
@@ -117,6 +122,7 @@
"@types/react-dom": "^18.0.10", "@types/react-dom": "^18.0.10",
"@types/react-table": "^7.7.9", "@types/react-table": "^7.7.9",
"duplicate-dependencies-webpack-plugin": "^1.0.2", "duplicate-dependencies-webpack-plugin": "^1.0.2",
"i18next-parser": "^8.0.0",
"jest": "25.5.4", "jest": "25.5.4",
"ts-jest": "25.5.1", "ts-jest": "25.5.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
@@ -0,0 +1,106 @@
module.exports = {
contextSeparator: "_",
// Key separator used in your translation keys
createOldCatalogs: false,
// Save the \_old files
defaultNamespace: "translation",
// Default namespace used in your i18next config
defaultValue: "",
// Default value to give to keys with no value
// You may also specify a function accepting the locale, namespace, key, and value as arguments
indentation: 2,
// Indentation of the catalog files
keepRemoved: false,
// Keep keys from the catalog that are no longer in code
keySeparator: false,
// Key separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
// see below for more details
lexers: {
hbs: ["HandlebarsLexer"],
handlebars: ["HandlebarsLexer"],
htm: ["HTMLLexer"],
html: ["HTMLLexer"],
mjs: ["JavascriptLexer"],
js: ["JavascriptLexer"], // if you're writing jsx inside .js files, change this to JsxLexer
ts: ["JavascriptLexer"],
jsx: ["JsxLexer"],
tsx: ["JsxLexer"],
default: ["JavascriptLexer"],
},
lineEnding: "auto",
// Control the line ending. See options at https://github.com/ryanve/eol
locales: ["en"],
// An array of the locales in your applications
namespaceSeparator: false,
// Namespace separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
output: "ui/public/locales/$LOCALE/$NAMESPACE.json",
// Supports $LOCALE and $NAMESPACE injection
// Supports JSON (.json) and YAML (.yml) file formats
// Where to write the locale files relative to process.cwd()
pluralSeparator: "_",
// Plural separator used in your translation keys
// If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
input: undefined,
// An array of globs that describe where to look for source files
// relative to the location of the configuration file
sort: false,
// Whether or not to sort the catalog. Can also be a [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters)
verbose: true,
// Display info about the parsing including some stats
failOnWarnings: true,
// Exit with an exit code of 1 on warnings
failOnUpdate: false,
// Exit with an exit code of 1 when translations are updated (for CI purpose)
customValueTemplate: null,
// If you wish to customize the value output the value as an object, you can set your own format.
// ${defaultValue} is the default value you set in your translation function.
// Any other custom property will be automatically extracted.
//
// Example:
// {
// message: "${defaultValue}",
// description: "${maxLength}", // t('my-key', {maxLength: 150})
// }
resetDefaultValueLocale: null,
// The locale to compare with default values to determine whether a default value has been changed.
// If this is set and a default value differs from a translation in the specified locale, all entries
// for that key across locales are reset to the default value, and existing translations are moved to
// the `_old` file.
i18nextOptions: null,
// If you wish to customize options in internally used i18next instance, you can define an object with any
// configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
// { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
yamlOptions: null,
// If you wish to customize options for yaml output, you can define an object here.
// Configuration options are here (https://github.com/nodeca/js-yaml#dump-object---options-).
// Example:
// {
// lineWidth: -1,
// }
}
File diff suppressed because it is too large. Load diff
@@ -21,25 +21,25 @@
} }
.emoji-picker-react .emoji { .emoji-picker-react .emoji {
color: #F3F4F6 !important; color: #f3f4f6 !important;
} }
.emoji-picker-react input.emoji-search { .emoji-picker-react input.emoji-search {
background-color: #F9FAFB !important; background-color: #f9fafb !important;
border-radius: 4px !important; border-radius: 4px !important;
border-color: #E5E7EB !important; border-color: #e5e7eb !important;
margin: 0 !important; margin: 0 !important;
width: 100% !important; width: 100% !important;
font-size: 12px !important; font-size: 12px !important;
font-family: "Inter" !important; font-family: "Inter" !important;
color: #111827 !important; color: #111827 !important;
caret-color: #7C3AED !important; caret-color: #7c3aed !important;
} }
.emoji-picker-react input.emoji-search::placeholder { .emoji-picker-react input.emoji-search::placeholder {
font-size: 12px !important; font-size: 12px !important;
font-family: "Inter" !important; font-family: "Inter" !important;
color: #9CA3AF !important; color: #9ca3af !important;
} }
.emoji-picker-react .emoji-categories button.icn-smileys_people { .emoji-picker-react .emoji-categories button.icn-smileys_people {
@@ -85,13 +85,15 @@
} }
.emoji-picker-react .emoji-categories { .emoji-picker-react .emoji-categories {
background-color: #F3F4F6 !important; background-color: #f3f4f6 !important;
padding: 4px !important; padding: 4px !important;
border-radius: 4px !important; border-radius: 4px !important;
margin-bottom: 8px !important; margin-bottom: 8px !important;
} }
.emoji-picker-react .active-category-indicator-wrapper .active-category-indicator { .emoji-picker-react
.active-category-indicator-wrapper
.active-category-indicator {
display: none !important; display: none !important;
} }
@@ -1,5 +1,6 @@
import clsx from "clsx" import clsx from "clsx"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import ArrowLeftIcon from "../../fundamentals/icons/arrow-left-icon" import ArrowLeftIcon from "../../fundamentals/icons/arrow-left-icon"
type Props = { type Props = {
@@ -8,7 +9,8 @@ type Props = {
className?: string className?: string
} }
const BackButton = ({ path, label = "Go back", className }: Props) => { const BackButton = ({ path, label, className }: Props) => {
const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
return ( return (
<button <button
@@ -19,7 +21,9 @@ const BackButton = ({ path, label = "Go back", className }: Props) => {
> >
<div className="gap-x-xsmall text-grey-50 inter-grey-40 inter-small-semibold flex items-center"> <div className="gap-x-xsmall text-grey-50 inter-grey-40 inter-small-semibold flex items-center">
<ArrowLeftIcon size={20} /> <ArrowLeftIcon size={20} />
<span className="ml-1">{label}</span> <span className="ml-1">
{label || t("back-button-go-back", "Go back")}
</span>
</div> </div>
</button> </button>
) )
@@ -2,14 +2,14 @@ import { Controller } from "react-hook-form"
import NestedMultiselect from "../../../../domain/categories/components/multiselect" import NestedMultiselect from "../../../../domain/categories/components/multiselect"
import { import {
FeatureFlag, FeatureFlag,
useFeatureFlag useFeatureFlag,
} from "../../../../providers/feature-flag-provider" } from "../../../../providers/feature-flag-provider"
import { Option } from "../../../../types/shared" import { Option } from "../../../../types/shared"
import { NestedForm } from "../../../../utils/nested-form" import { NestedForm } from "../../../../utils/nested-form"
import InputHeader from "../../../fundamentals/input-header" import InputHeader from "../../../fundamentals/input-header"
import { import {
NextCreateableSelect, NextCreateableSelect,
NextSelect NextSelect,
} from "../../../molecules/select/next-select" } from "../../../molecules/select/next-select"
import TagInput from "../../../molecules/tag-input" import TagInput from "../../../molecules/tag-input"
import useOrganizeData from "./use-organize-data" import useOrganizeData from "./use-organize-data"
@@ -3,9 +3,7 @@ import { FormImage } from "../../../../types/shared"
import { NestedForm } from "../../../../utils/nested-form" import { NestedForm } from "../../../../utils/nested-form"
import FileUploadField from "../../../atoms/file-upload-field" import FileUploadField from "../../../atoms/file-upload-field"
import TrashIcon from "../../../fundamentals/icons/trash-icon" import TrashIcon from "../../../fundamentals/icons/trash-icon"
import Actionables, { import Actionables, { ActionType } from "../../../molecules/actionables"
ActionType
} from "../../../molecules/actionables"
export type ThumbnailFormType = { export type ThumbnailFormType = {
images: FormImage[] images: FormImage[]
@@ -3,6 +3,7 @@ import { useAdminSalesChannels } from "medusa-react"
import React from "react" import React from "react"
import Tooltip from "../../atoms/tooltip" import Tooltip from "../../atoms/tooltip"
import Badge from "../../fundamentals/badge" import Badge from "../../fundamentals/badge"
import { Trans, useTranslation } from "react-i18next"
type Props = { type Props = {
channels?: SalesChannel[] channels?: SalesChannel[]
@@ -11,7 +12,10 @@ type Props = {
const SalesChannelsDisplay = ({ channels = [] }: Props) => { const SalesChannelsDisplay = ({ channels = [] }: Props) => {
const { count } = useAdminSalesChannels() const { count } = useAdminSalesChannels()
const remainder = Math.max(channels.length - 3, 0) const remainder = Math.max(channels.length - 3, 0)
const { t } = useTranslation()
const availableChannelsCount = channels.length ? channels.length : 0
const totalChannelsCount = count || 0
return ( return (
<div className="gap-y-small flex flex-col"> <div className="gap-y-small flex flex-col">
{channels.length > 0 && ( {channels.length > 0 && (
@@ -41,13 +45,21 @@ const SalesChannelsDisplay = ({ channels = [] }: Props) => {
</div> </div>
)} )}
<p className="inter-base-regular text-grey-50"> <p className="inter-base-regular text-grey-50">
<Trans
i18nKey="sales-channels-display-available-count"
availableChannelsCount={availableChannelsCount}
totalChannelsCount={totalChannelsCount}
>
Available in{" "} Available in{" "}
<span className="inter-base-semibold text-grey-90"> <span className="inter-base-semibold text-grey-90">
{channels.length ? channels.length : 0} {{ availableChannelsCount }}
</span>{" "} </span>{" "}
out of{" "} out of{" "}
<span className="inter-base-semibold text-grey-90">{count || 0}</span>{" "} <span className="inter-base-semibold text-grey-90">
{{ totalChannelsCount }}
</span>{" "}
Sales Channels Sales Channels
</Trans>
</p> </p>
</div> </div>
) )
@@ -1,4 +1,5 @@
import React, { useEffect } from "react" import React, { useEffect } from "react"
import { useTranslation } from "react-i18next"
import useOutsideClick from "../../../hooks/use-outside-click" import useOutsideClick from "../../../hooks/use-outside-click"
import { usePolling } from "../../../providers/polling-provider" 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" import BatchJobActivityList from "../batch-jobs-activity-list"
const ActivityDrawer = ({ onDismiss }) => { const ActivityDrawer = ({ onDismiss }) => {
const { t } = useTranslation()
const ref = React.useRef<HTMLDivElement>(null) const ref = React.useRef<HTMLDivElement>(null)
const { batchJobs, hasPollingError, refetch } = usePolling() const { batchJobs, hasPollingError, refetch } = usePolling()
useOutsideClick(onDismiss, ref) useOutsideClick(onDismiss, ref)
@@ -21,7 +23,9 @@ const ActivityDrawer = ({ onDismiss }) => {
ref={ref} 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" 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 ? ( {!hasPollingError ? (
batchJobs ? ( batchJobs ? (
@@ -37,33 +41,44 @@ const ActivityDrawer = ({ onDismiss }) => {
} }
const EmptyActivityDrawer = () => { const EmptyActivityDrawer = () => {
const { t } = useTranslation()
return ( return (
<div className="flex h-full w-full flex-col items-center justify-center p-4"> <div className="flex h-full w-full flex-col items-center justify-center p-4">
<SidedMouthFaceIcon size={36} /> <SidedMouthFaceIcon size={36} />
<span className={"inter-large-semibold text-grey-90 mt-4"}> <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>
<span className={"text-grey-60 inter-base-regular mt-4 text-center"}> <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 {t(
will live here. "activity-drawer-no-notifications-description",
"You don't have any notifications at the moment, but once you do they will live here."
)}
</span> </span>
</div> </div>
) )
} }
const ErrorActivityDrawer = () => { const ErrorActivityDrawer = () => {
const { t } = useTranslation()
return ( return (
<div className="flex h-full w-full flex-col items-center justify-center p-4"> <div className="flex h-full w-full flex-col items-center justify-center p-4">
<SadFaceIcon size={36} /> <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"}> <span className={"text-grey-60 inter-base-regular mt-2 text-center"}>
Something went wrong while trying to fetch your notifications - We will {t(
keep trying! "activity-drawer-error-description",
"Something went wrong while trying to fetch your notifications - We will keep trying!"
)}
</span> </span>
<div className="mt-4 flex items-center"> <div className="mt-4 flex items-center">
<Spinner size={"small"} variant={"secondary"} /> <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>
</div> </div>
) )
@@ -1,6 +1,7 @@
import clsx from "clsx" import clsx from "clsx"
import { useEffect } from "react" import { useEffect } from "react"
import { Controller, useWatch } from "react-hook-form" import { Controller, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { NestedForm } from "../../../utils/nested-form" import { NestedForm } from "../../../utils/nested-form"
import Switch from "../../atoms/switch" import Switch from "../../atoms/switch"
import InfoIcon from "../../fundamentals/icons/info-icon" import InfoIcon from "../../fundamentals/icons/info-icon"
@@ -18,6 +19,7 @@ type Props = {
const AnalyticsConfigForm = ({ form, compact }: Props) => { const AnalyticsConfigForm = ({ form, compact }: Props) => {
const { control, setValue, path } = form const { control, setValue, path } = form
const { t } = useTranslation()
const watchOptOut = useWatch({ const watchOptOut = useWatch({
control, control,
@@ -41,13 +43,14 @@ const AnalyticsConfigForm = ({ form, compact }: Props) => {
<div className="gap-y-2xsmall flex flex-1 flex-col"> <div className="gap-y-2xsmall flex flex-1 flex-col">
<div className="flex items-center"> <div className="flex items-center">
<h2 className="inter-base-semibold mr-2"> <h2 className="inter-base-semibold mr-2">
Anonymize my usage data{" "} {t("analytics-config-form-title", "Anonymize my usage data")}
</h2> </h2>
{compact && ( {compact && (
<Tooltip <Tooltip
content="You can choose to anonymize your usage data. If this option is content={t(
selected, we will not collect your personal information, such as "analytics-config-form-description",
your name and email address." "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" side="top"
> >
<InfoIcon size="18px" color={"#889096"} /> <InfoIcon size="18px" color={"#889096"} />
@@ -56,9 +59,10 @@ const AnalyticsConfigForm = ({ form, compact }: Props) => {
</div> </div>
{!compact && ( {!compact && (
<p className="inter-base-regular text-grey-50"> <p className="inter-base-regular text-grey-50">
You can choose to anonymize your usage data. If this option is {t(
selected, we will not collect your personal information, such as "analytics-config-form-description",
your name and email address. "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> </p>
)} )}
</div> </div>
@@ -80,11 +84,17 @@ const AnalyticsConfigForm = ({ form, compact }: Props) => {
<div className="gap-y-2xsmall flex flex-1 flex-col"> <div className="gap-y-2xsmall flex flex-1 flex-col">
<div className="flex items-center"> <div className="flex items-center">
<h2 className="inter-base-semibold mr-2"> <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> </h2>
{compact && ( {compact && (
<Tooltip <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" side="top"
> >
<InfoIcon size="18px" color={"#889096"} /> <InfoIcon size="18px" color={"#889096"} />
@@ -93,7 +103,10 @@ const AnalyticsConfigForm = ({ form, compact }: Props) => {
</div> </div>
{!compact && ( {!compact && (
<p className="inter-base-regular text-grey-50"> <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> </p>
)} )}
</div> </div>
@@ -1,5 +1,6 @@
import clsx from "clsx" import clsx from "clsx"
import { useForm, useWatch } from "react-hook-form" import { useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { useAnalytics } from "../../../providers/analytics-provider" import { useAnalytics } from "../../../providers/analytics-provider"
import { useAdminCreateAnalyticsConfig } from "../../../services/analytics" import { useAdminCreateAnalyticsConfig } from "../../../services/analytics"
@@ -18,6 +19,7 @@ type AnalyticsPreferenceFormType = {
} }
const AnalyticsPreferencesModal = () => { const AnalyticsPreferencesModal = () => {
const { t } = useTranslation()
const notification = useNotification() const notification = useNotification()
const { mutate, isLoading } = useAdminCreateAnalyticsConfig() const { mutate, isLoading } = useAdminCreateAnalyticsConfig()
@@ -56,8 +58,11 @@ const AnalyticsPreferencesModal = () => {
mutate(config, { mutate(config, {
onSuccess: () => { onSuccess: () => {
notification( notification(
"Success", t("analytics-preferences-success", "Success"),
"Your preferences were successfully updated", t(
"analytics-preferences-your-preferences-were-successfully-updated",
"Your preferences were successfully updated"
),
"success" "success"
) )
@@ -68,7 +73,11 @@ const AnalyticsPreferencesModal = () => {
setSubmittingConfig(false) setSubmittingConfig(false)
}, },
onError: (err) => { onError: (err) => {
notification("Error", getErrorMessage(err), "error") notification(
t("analytics-preferences-error", "Error"),
getErrorMessage(err),
"error"
)
setSubmittingConfig(false) setSubmittingConfig(false)
}, },
}) })
@@ -80,27 +89,29 @@ const AnalyticsPreferencesModal = () => {
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="mt-5xlarge flex w-full max-w-[664px] flex-col"> <div className="mt-5xlarge flex w-full max-w-[664px] flex-col">
<h1 className="inter-xlarge-semibold mb-large"> <h1 className="inter-xlarge-semibold mb-large">
Help us get better {t(
"analytics-preferences-help-us-get-better",
"Help us get better"
)}
</h1> </h1>
<p className="text-grey-50"> <p className="text-grey-50">
To create the most compelling e-commerce experience we would like {t(
to gain insights in how you use Medusa. User insights allow us to "analytics-preferences-disclaimer",
build a better, more engaging, and more usable products. We only "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"
collect data for product improvements. Read what data we gather in )}{" "}
our{" "}
<a <a
href="https://docs.medusajs.com/usage" href="https://docs.medusajs.com/usage"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
className="text-violet-60" className="text-violet-60"
> >
documentation {t("analytics-preferences-documentation", "documentation")}
</a> </a>
. .
</p> </p>
<div className="mt-xlarge gap-y-xlarge flex flex-col"> <div className="mt-xlarge gap-y-xlarge flex flex-col">
<InputField <InputField
label="Email" label={"Email"}
placeholder="you@company.com" placeholder="you@company.com"
disabled={watchOptOut || watchAnonymize} disabled={watchOptOut || watchAnonymize}
className={clsx("transition-opacity", { className={clsx("transition-opacity", {
@@ -108,7 +119,10 @@ const AnalyticsPreferencesModal = () => {
})} })}
{...register("email", { {...register("email", {
pattern: { 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, value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
}, },
})} })}
@@ -123,7 +137,7 @@ const AnalyticsPreferencesModal = () => {
loading={isLoading} loading={isLoading}
onClick={onSubmit} onClick={onSubmit}
> >
Continue {t("analytics-preferences-continue", "Continue")}
</Button> </Button>
</div> </div>
</div> </div>
@@ -18,6 +18,7 @@ import PlusIcon from "../../fundamentals/icons/plus-icon"
import InputHeader from "../../fundamentals/input-header" import InputHeader from "../../fundamentals/input-header"
import Input from "../../molecules/input" import Input from "../../molecules/input"
import Select from "../../molecules/select" import Select from "../../molecules/select"
import { useTranslation } from "react-i18next"
type CurrencyInputProps = { type CurrencyInputProps = {
currencyCodes?: string[] currencyCodes?: string[]
@@ -74,6 +75,7 @@ const Root: React.FC<CurrencyInputProps> = ({
label: code.toUpperCase(), label: code.toUpperCase(),
value: code, value: code,
})) ?? [] })) ?? []
const { t } = useTranslation()
const [selectedCurrency, setSelectedCurrency] = useState< const [selectedCurrency, setSelectedCurrency] = useState<
CurrencyType | undefined CurrencyType | undefined
@@ -132,7 +134,7 @@ const Root: React.FC<CurrencyInputProps> = ({
{!readOnly ? ( {!readOnly ? (
<Select <Select
enableSearch enableSearch
label="Currency" label={t("currency-input-currency", "Currency")}
value={value} value={value}
onChange={onCurrencyChange} onChange={onCurrencyChange}
options={options} options={options}
@@ -140,7 +142,7 @@ const Root: React.FC<CurrencyInputProps> = ({
/> />
) : ( ) : (
<Input <Input
label="Currency" label={t("currency-input-currency", "Currency")}
value={value?.label} value={value?.label}
readOnly readOnly
className="pointer-events-none" className="pointer-events-none"
@@ -172,6 +174,8 @@ const Amount = forwardRef<HTMLInputElement, AmountInputProps>(
}: AmountInputProps, }: AmountInputProps,
ref ref
) => { ) => {
const { t } = useTranslation()
const { currencyInfo } = useContext(CurrencyContext) const { currencyInfo } = useContext(CurrencyContext)
const [invalid, setInvalid] = useState<boolean>(false) const [invalid, setInvalid] = useState<boolean>(false)
const [formattedValue, setFormattedValue] = useState<string | undefined>( const [formattedValue, setFormattedValue] = useState<string | undefined>(
@@ -250,7 +254,10 @@ const Amount = forwardRef<HTMLInputElement, AmountInputProps>(
<Tooltip <Tooltip
open={invalid} open={invalid}
side={"top"} 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"> <span className="inter-base-regular text-grey-40 mr-xsmall">
{currencyInfo.symbol_native} {currencyInfo.symbol_native}
@@ -1,4 +1,5 @@
import React, { useState } from "react" import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import useNotification from "../../hooks/use-notification" import useNotification from "../../hooks/use-notification"
import { getErrorMessage } from "../../utils/error-messages" import { getErrorMessage } from "../../utils/error-messages"
@@ -16,14 +17,15 @@ type DeletePromptProps = {
} }
const DeletePrompt: React.FC<DeletePromptProps> = ({ const DeletePrompt: React.FC<DeletePromptProps> = ({
heading = "Are you sure you want to delete?", heading,
text = "", text = "",
successText = "Delete successful", successText,
cancelText = "No, cancel", cancelText,
confirmText = "Yes, remove", confirmText,
handleClose, handleClose,
onDelete, onDelete,
}) => { }) => {
const { t } = useTranslation()
const notification = useNotification() const notification = useNotification()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
@@ -34,7 +36,12 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
onDelete() onDelete()
.then(() => { .then(() => {
if (successText) { 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")) .catch((err) => notification("Error", getErrorMessage(err), "error"))
@@ -49,7 +56,13 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
<Modal.Body> <Modal.Body>
<Modal.Content> <Modal.Content>
<div className="flex flex-col"> <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> <span className="inter-base-regular text-grey-50 mt-1">{text}</span>
</div> </div>
</Modal.Content> </Modal.Content>
@@ -61,7 +74,7 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
size="small" size="small"
onClick={handleClose} onClick={handleClose}
> >
{cancelText} {cancelText || t("organisms-no-cancel", "No, cancel")}
</Button> </Button>
<Button <Button
loading={isLoading} loading={isLoading}
@@ -71,7 +84,7 @@ const DeletePrompt: React.FC<DeletePromptProps> = ({
onClick={handleSubmit} onClick={handleSubmit}
disabled={isLoading} disabled={isLoading}
> >
{confirmText} {confirmText || t("organisms-yes-remove", "Yes, remove")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,6 +1,7 @@
import * as RadixCollapsible from "@radix-ui/react-collapsible" import * as RadixCollapsible from "@radix-ui/react-collapsible"
import clsx from "clsx" import clsx from "clsx"
import React, { useState } from "react" import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import ArrowDownIcon from "../../fundamentals/icons/arrow-down-icon" import ArrowDownIcon from "../../fundamentals/icons/arrow-down-icon"
import ArrowUpIcon from "../../fundamentals/icons/arrow-up-icon" import ArrowUpIcon from "../../fundamentals/icons/arrow-up-icon"
@@ -17,10 +18,19 @@ const DetailsCollapsible = ({
contentProps, contentProps,
children, children,
}: DetailsCollapsibleProps) => { }: DetailsCollapsibleProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const Icon = open ? ArrowUpIcon : ArrowDownIcon 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 ( return (
<RadixCollapsible.Root <RadixCollapsible.Root
@@ -2,6 +2,7 @@ import { User } from "@medusajs/medusa"
import { useAdminUpdateUser } from "medusa-react" import { useAdminUpdateUser } from "medusa-react"
import React, { useEffect } from "react" import React, { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
import FormValidator from "../../../utils/form-validator" import FormValidator from "../../../utils/form-validator"
@@ -33,6 +34,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
formState: { errors }, formState: { errors },
} = useForm<EditUserModalFormData>() } = useForm<EditUserModalFormData>()
const notification = useNotification() const notification = useNotification()
const { t } = useTranslation()
useEffect(() => { useEffect(() => {
reset(mapUser(user)) reset(mapUser(user))
@@ -41,11 +43,19 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
const onSubmit = (data: EditUserModalFormData) => { const onSubmit = (data: EditUserModalFormData) => {
mutate(data, { mutate(data, {
onSuccess: () => { 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() onSuccess()
}, },
onError: (error) => { onError: (error) => {
notification("Error", getErrorMessage(error), "error") notification(
t("edit-user-modal-error", "Error"),
getErrorMessage(error),
"error"
)
}, },
onSettled: () => { onSettled: () => {
handleClose() handleClose()
@@ -58,13 +68,18 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={handleClose}> <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.Header>
<Modal.Content> <Modal.Content>
<div className="gap-large mb-base grid w-full grid-cols-2"> <div className="gap-large mb-base grid w-full grid-cols-2">
<InputField <InputField
label="First Name" label={t("edit-user-modal-first-name-label", "First Name")}
placeholder="First name..." placeholder={t(
"edit-user-modal-first-name-placeholder",
"First name..."
)}
required required
{...register("first_name", { {...register("first_name", {
required: FormValidator.required("First name"), required: FormValidator.required("First name"),
@@ -74,8 +89,11 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
errors={errors} errors={errors}
/> />
<InputField <InputField
label="Last Name" label={t("edit-user-modal-last-name-label", "Last Name")}
placeholder="Last name..." placeholder={t(
"edit-user-modal-last-name-placeholder",
"Last name..."
)}
required required
{...register("last_name", { {...register("last_name", {
required: FormValidator.required("Last name"), required: FormValidator.required("Last name"),
@@ -85,7 +103,11 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
errors={errors} errors={errors}
/> />
</div> </div>
<InputField label="Email" disabled value={user.email} /> <InputField
label={t("edit-user-modal-email", "Email")}
disabled
value={user.email}
/>
</Modal.Content> </Modal.Content>
<Modal.Footer> <Modal.Footer>
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
@@ -95,7 +117,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
onClick={handleClose} onClick={handleClose}
className="mr-2" className="mr-2"
> >
Cancel {t("edit-user-modal-cancel", "Cancel")}
</Button> </Button>
<Button <Button
loading={isLoading} loading={isLoading}
@@ -103,7 +125,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
variant="primary" variant="primary"
size="small" size="small"
> >
Save {t("edit-user-modal-save", "Save")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,9 +1,11 @@
import { AxiosError } from "axios" import { AxiosError } from "axios"
import React, { ErrorInfo } from "react" import React, { ErrorInfo } from "react"
import { analyticsOptIn } from "../../../services/analytics" import { analyticsOptIn } from "../../../services/analytics"
import { Translation } from "react-i18next"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import { WRITE_KEY } from "../../../constants/analytics" import { WRITE_KEY } from "../../../constants/analytics"
import { AnalyticsBrowser } from "@segment/analytics-next" import { AnalyticsBrowser } from "@segment/analytics-next"
import { TFunction } from "i18next"
type State = { type State = {
hasError: boolean hasError: boolean
@@ -16,7 +18,7 @@ type Props = {
} }
// Analytics instance used for tracking errors // Analytics instance used for tracking errors
let analyticsInstance: ReturnType<typeof AnalyticsBrowser.load> | undefined; let analyticsInstance: ReturnType<typeof AnalyticsBrowser.load> | undefined
const analytics = () => { const analytics = () => {
if (!analyticsInstance) { if (!analyticsInstance) {
@@ -75,10 +77,14 @@ class ErrorBoundary extends React.Component<Props, State> {
</p> </p>
)} )}
<h1 className="inter-xlarge-semibold mb-xsmall"> <h1 className="inter-xlarge-semibold mb-xsmall">
{errorMessage(this.state.status)} <Translation>
{(t) => errorMessage(t, this.state.status)}
</Translation>
</h1> </h1>
<p className="inter-base-regular text-grey-50"> <p className="inter-base-regular text-grey-50">
{errorDescription(this.state.status)} <Translation>
{(t) => errorDescription(t, this.state.status)}
</Translation>
</p> </p>
</div> </div>
@@ -88,7 +94,11 @@ class ErrorBoundary extends React.Component<Props, State> {
variant="primary" variant="primary"
onClick={this.dismissError} onClick={this.dismissError}
> >
Back to dashboard <Translation>
{(t) =>
t("error-boundary-back-to-dashboard", "Back to dashboard")
}
</Translation>
</Button> </Button>
</div> </div>
</div> </div>
@@ -112,43 +122,72 @@ const shouldTrackEvent = async (error: Error) => {
return false return false
} }
return await analyticsOptIn(); return await analyticsOptIn()
} }
const errorMessage = (status?: number) => { const errorMessage = (t: TFunction, status?: number) => {
const defaultMessage = "An unknown error occured" const defaultMessage = t(
"error-boundary-an-unknown-error-occured",
"An unknown error occured"
)
if (!status) { if (!status) {
return defaultMessage return defaultMessage
} }
const message = { const message = {
400: "Bad request", 400: t("error-boundary-bad-request", "Bad request"),
401: "You are not logged in", 401: t("error-boundary-you-are-not-logged-in", "You are not logged in"),
403: "You do not have permission perform this action", 403: t(
404: "Page was not found", "error-boundary-you-do-not-have-permission-perform-this-action",
500: "An unknown server error occured", "You do not have permission perform this action"
503: "Server is currently unavailable", ),
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] }[status]
return message || defaultMessage return message || defaultMessage
} }
const errorDescription = (status?: number) => { const errorDescription = (t: TFunction, status?: number) => {
const defaultDescription = 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." "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) { if (!status) {
return defaultDescription return defaultDescription
} }
const description = { const description = {
400: "The request was malformed, fix your request and please try again.", 400: t(
401: "You are not logged in, please log in to proceed.", "error-boundary-400",
403: "You do not have permission perform this action, if you think this is a mistake, contact your administrator.", "The request was malformed, fix your request and please try again."
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.", 401: t(
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.", "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] }[status]
return description || defaultDescription return description || defaultDescription
@@ -1,4 +1,5 @@
import React from "react" import React from "react"
import { useTranslation } from "react-i18next"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal" import Modal from "../../molecules/modal"
@@ -15,6 +16,7 @@ const ExportModal: React.FC<ExportModalProps> = ({
loading, loading,
onSubmit, onSubmit,
}) => { }) => {
const { t } = useTranslation()
return ( return (
<Modal handleClose={handleClose}> <Modal handleClose={handleClose}>
<Modal.Body> <Modal.Body>
@@ -30,7 +32,7 @@ const ExportModal: React.FC<ExportModalProps> = ({
overview. overview.
</div> */} </div> */}
<div className="inter-small-regular text-grey-50 mb-4 flex"> <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> </div>
</Modal.Content> </Modal.Content>
<Modal.Footer> <Modal.Footer>
@@ -41,7 +43,7 @@ const ExportModal: React.FC<ExportModalProps> = ({
onClick={handleClose} onClick={handleClose}
className="mr-2" className="mr-2"
> >
Cancel {t("export-modal-cancel", "Cancel")}
</Button> </Button>
<Button <Button
loading={loading} loading={loading}
@@ -50,7 +52,7 @@ const ExportModal: React.FC<ExportModalProps> = ({
size="small" size="small"
onClick={onSubmit} onClick={onSubmit}
> >
Export {t("export-modal-export", "Export")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,4 +1,5 @@
import React from "react" import React from "react"
import { useTranslation } from "react-i18next"
import FileUploadField from "../../atoms/file-upload-field" import FileUploadField from "../../atoms/file-upload-field"
import Modal from "../../molecules/modal" import Modal from "../../molecules/modal"
@@ -13,11 +14,14 @@ const FileUploadModal: React.FC<FileUploadModalProps> = ({
filetypes, filetypes,
setFiles, setFiles,
}) => { }) => {
const { t } = useTranslation()
return ( return (
<Modal handleClose={handleClose}> <Modal handleClose={handleClose}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={handleClose}> <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.Header>
<Modal.Content> <Modal.Content>
<div className="h-96"> <div className="h-96">
@@ -1,4 +1,5 @@
import React, { useMemo } from "react" import React, { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { normalizeAmount } from "../../../utils/prices" import { normalizeAmount } from "../../../utils/prices"
import EditIcon from "../../fundamentals/icons/edit-icon" import EditIcon from "../../fundamentals/icons/edit-icon"
import TrashIcon from "../../fundamentals/icons/trash-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 StatusIndicator from "../../fundamentals/status-indicator"
import { ActionType } from "../../molecules/actionables" import { ActionType } from "../../molecules/actionables"
import BannerCard from "../../molecules/banner-card" import BannerCard from "../../molecules/banner-card"
import TagGrid from "../../molecules/tag-grid.tsx" import TagGrid from "../../molecules/tag-grid"
type GiftCardVariant = { type GiftCardVariant = {
prices: { prices: {
@@ -38,19 +39,23 @@ const GiftCardBanner: React.FC<GiftCardBannerProps> = ({
onUnpublish, onUnpublish,
onDelete, onDelete,
}) => { }) => {
const { t } = useTranslation()
const actions: ActionType[] = [ const actions: ActionType[] = [
{ {
label: "Edit", label: t("gift-card-banner-edit", "Edit"),
onClick: onEdit, onClick: onEdit,
icon: <EditIcon size={16} />, 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, onClick: onUnpublish,
icon: <UnpublishIcon size={16} />, icon: <UnpublishIcon size={16} />,
}, },
{ {
label: "Delete", label: t("gift-card-banner-delete", "Delete"),
onClick: onDelete, onClick: onDelete,
icon: <TrashIcon size={16} />, icon: <TrashIcon size={16} />,
variant: "danger", variant: "danger",
@@ -84,7 +89,11 @@ const GiftCardBanner: React.FC<GiftCardBannerProps> = ({
<TagGrid tags={denominations} badgeVariant="default" /> <TagGrid tags={denominations} badgeVariant="default" />
<StatusIndicator <StatusIndicator
variant={status === "published" ? "success" : "danger"} 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> </div>
</BannerCard.Footer> </BannerCard.Footer>
@@ -2,6 +2,7 @@ import { Product } from "@medusajs/medusa"
import { useAdminCreateVariant, useAdminStore } from "medusa-react" import { useAdminCreateVariant, useAdminStore } from "medusa-react"
import { useCallback, useMemo } from "react" import { useCallback, useMemo } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form" import { nestedForm } from "../../../utils/nested-form"
@@ -31,6 +32,7 @@ type AddDenominationModalFormType = {
} }
const AddDenominationModal = ({ open, onClose, giftCard }: Props) => { const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
const { t } = useTranslation()
const { mutate, isLoading: isMutating } = useAdminCreateVariant(giftCard.id) const { mutate, isLoading: isMutating } = useAdminCreateVariant(giftCard.id)
const { store } = useAdminStore() const { store } = useAdminStore()
@@ -114,8 +116,14 @@ const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
mutate(payload, { mutate(payload, {
onSuccess: () => { onSuccess: () => {
notification( notification(
"Denomination added", t(
"A new denomination was successfully added", "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" "success"
) )
handleClose() handleClose()
@@ -124,13 +132,20 @@ const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
const errorMessage = () => { const errorMessage = () => {
// @ts-ignore // @ts-ignore
if (error.response?.data?.type === "duplicate_error") { 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 { } else {
return getErrorMessage(error) 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 open={open} handleClose={handleClose}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={handleClose}> <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> </Modal.Header>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Modal.Content> <Modal.Content>
@@ -153,7 +173,7 @@ const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
type="button" type="button"
onClick={handleClose} onClick={handleClose}
> >
Cancel {t("gift-card-denominations-section-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
@@ -162,7 +182,10 @@ const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
disabled={isMutating || !isDirty} disabled={isMutating || !isDirty}
loading={isMutating} loading={isMutating}
> >
Save and close {t(
"gift-card-denominations-section-save-and-close",
"Save and close"
)}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -7,6 +7,7 @@ import {
} from "medusa-react" } from "medusa-react"
import { useCallback, useEffect } from "react" import { useCallback, useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form" import { nestedForm } from "../../../utils/nested-form"
@@ -31,6 +32,7 @@ const EditDenominationsModal = ({
onClose, onClose,
open, open,
}: EditDenominationsModalProps) => { }: EditDenominationsModalProps) => {
const { t } = useTranslation()
const { store } = useAdminStore() const { store } = useAdminStore()
const { mutate, isLoading } = useAdminUpdateVariant(denomination.product_id) const { mutate, isLoading } = useAdminUpdateVariant(denomination.product_id)
@@ -91,15 +93,25 @@ const EditDenominationsModal = ({
{ {
onSuccess: () => { onSuccess: () => {
notification( notification(
"Denomination updated", t(
"A new denomination was successfully updated", "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" "success"
) )
queryClient.invalidateQueries(adminProductKeys.all) queryClient.invalidateQueries(adminProductKeys.all)
handleClose() handleClose()
}, },
onError: (error) => { 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 open={open} handleClose={handleClose}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={handleClose}> <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> </Modal.Header>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Modal.Content> <Modal.Content>
@@ -123,7 +140,7 @@ const EditDenominationsModal = ({
type="button" type="button"
onClick={handleClose} onClick={handleClose}
> >
Cancel {t("gift-card-denominations-section-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
@@ -132,7 +149,10 @@ const EditDenominationsModal = ({
loading={isLoading} loading={isLoading}
disabled={!isDirty || isLoading} disabled={!isDirty || isLoading}
> >
Save and close {t(
"gift-card-denominations-section-save-and-close",
"Save and close"
)}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,4 +1,5 @@
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import useToggleState from "../../../hooks/use-toggle-state" import useToggleState from "../../../hooks/use-toggle-state"
import PlusIcon from "../../fundamentals/icons/plus-icon" import PlusIcon from "../../fundamentals/icons/plus-icon"
import Section from "../section" import Section from "../section"
@@ -12,6 +13,7 @@ type GiftCardDenominationsSectionProps = {
const GiftCardDenominationsSection = ({ const GiftCardDenominationsSection = ({
giftCard, giftCard,
}: GiftCardDenominationsSectionProps) => { }: GiftCardDenominationsSectionProps) => {
const { t } = useTranslation()
const { const {
state: addDenomination, state: addDenomination,
close: closeAddDenomination, close: closeAddDenomination,
@@ -21,11 +23,17 @@ const GiftCardDenominationsSection = ({
return ( return (
<> <>
<Section <Section
title="Denominations" title={t(
"gift-card-denominations-section-denominations",
"Denominations"
)}
forceDropdown forceDropdown
actions={[ actions={[
{ {
label: "Add Denomination", label: t(
"gift-card-denominations-section-add-denomination",
"Add Denomination"
),
onClick: openAddDenomination, onClick: openAddDenomination,
icon: <PlusIcon size={20} />, icon: <PlusIcon size={20} />,
}, },
@@ -2,6 +2,7 @@ import { MoneyAmount, ProductVariant } from "@medusajs/medusa"
import { createColumnHelper } from "@tanstack/react-table" import { createColumnHelper } from "@tanstack/react-table"
import { useAdminDeleteVariant, useAdminStore } from "medusa-react" import { useAdminDeleteVariant, useAdminStore } from "medusa-react"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import useImperativeDialog from "../../../hooks/use-imperative-dialog" import useImperativeDialog from "../../../hooks/use-imperative-dialog"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state" import useToggleState from "../../../hooks/use-toggle-state"
@@ -17,6 +18,7 @@ const columnHelper = createColumnHelper<ProductVariant>()
export const useDenominationColumns = () => { export const useDenominationColumns = () => {
const { store } = useAdminStore() const { store } = useAdminStore()
const { t } = useTranslation()
const columns = useMemo(() => { const columns = useMemo(() => {
if (!store) { if (!store) {
@@ -27,7 +29,10 @@ export const useDenominationColumns = () => {
return [ return [
columnHelper.display({ columnHelper.display({
header: "Denomination", header: t(
"gift-card-denominations-section-denomination",
"Denomination"
),
id: "denomination", id: "denomination",
cell: ({ row }) => { cell: ({ row }) => {
const defaultDenomination = row.original.prices.find( const defaultDenomination = row.original.prices.find(
@@ -50,7 +55,10 @@ export const useDenominationColumns = () => {
}, },
}), }),
columnHelper.display({ columnHelper.display({
header: "In other currencies", header: t(
"gift-card-denominations-section-in-other-currencies",
"In other currencies"
),
id: "other_currencies", id: "other_currencies",
cell: ({ row }) => { cell: ({ row }) => {
const otherCurrencies = row.original.prices.filter( const otherCurrencies = row.original.prices.filter(
@@ -98,7 +106,13 @@ export const useDenominationColumns = () => {
</ul> </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> </Tooltip>
)} )}
</p> </p>
@@ -118,6 +132,7 @@ export const useDenominationColumns = () => {
} }
const Actions = ({ original }: { original: ProductVariant }) => { const Actions = ({ original }: { original: ProductVariant }) => {
const { t } = useTranslation()
const { state, open, close } = useToggleState() const { state, open, close } = useToggleState()
const { mutateAsync } = useAdminDeleteVariant(original.product_id) const { mutateAsync } = useAdminDeleteVariant(original.product_id)
@@ -127,21 +142,37 @@ const Actions = ({ original }: { original: ProductVariant }) => {
const onDelete = async () => { const onDelete = async () => {
const shouldDelete = await dialog({ const shouldDelete = await dialog({
heading: "Delete denomination", heading: t(
text: "Are you sure you want to delete this denomination?", "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) { if (shouldDelete) {
mutateAsync(original.id, { mutateAsync(original.id, {
onSuccess: () => { onSuccess: () => {
notification( notification(
"Denomination deleted", t(
"Denomination was successfully deleted", "gift-card-denominations-section-denomination-deleted",
"Denomination deleted"
),
t(
"gift-card-denominations-section-denomination-was-successfully-deleted",
"Denomination was successfully deleted"
),
"success" "success"
) )
}, },
onError: (error) => { 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[] = [ const actions: ActionType[] = [
{ {
label: "Edit", label: t("gift-card-denominations-section-edit", "Edit"),
onClick: open, onClick: open,
icon: <EditIcon size={20} />, icon: <EditIcon size={20} />,
}, },
{ {
label: "Delete", label: t("gift-card-denominations-section-delete", "Delete"),
onClick: onDelete, onClick: onDelete,
icon: <TrashIcon size={20} />, icon: <TrashIcon size={20} />,
variant: "danger", variant: "danger",
@@ -1,4 +1,5 @@
import React, { useState } from "react" import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import DiscordIcon from "../../fundamentals/icons/discord-icon" import DiscordIcon from "../../fundamentals/icons/discord-icon"
import InputField from "../../molecules/input" import InputField from "../../molecules/input"
@@ -12,6 +13,7 @@ type MailDialogProps = {
} }
const MailDialog = ({ open, onClose }: MailDialogProps) => { const MailDialog = ({ open, onClose }: MailDialogProps) => {
const { t } = useTranslation()
const [subject, setSubject] = useState("") const [subject, setSubject] = useState("")
const [body, setBody] = useState("") const [body, setBody] = useState("")
const [link, setLink] = useState("mailto:support@medusajs.com") 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"> <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> <div>
<Dialog.Title className="inter-xlarge-semibold mb-1"> <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.Title>
<Dialog.Description className="inter-small-regular text-grey-50 mb-6"> <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> </Dialog.Description>
<InputField <InputField
label={"Subject"} label={t("help-dialog-subject", "Subject")}
value={subject} value={subject}
className="mb-4" 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)} onChange={(e) => setSubject(e.target.value)}
/> />
<TextArea <TextArea
label={"How can we help?"} label={t("help-dialog-how-can-we-help", "How can we help?")}
placeholder="Write a message..." placeholder={t(
"help-dialog-write-a-message",
"Write a message..."
)}
value={body} value={body}
onChange={(e) => { onChange={(e) => {
setBody(e.target.value) setBody(e.target.value)
@@ -65,15 +76,21 @@ const MailDialog = ({ open, onClose }: MailDialogProps) => {
<DiscordIcon size={24} /> <DiscordIcon size={24} />
</span> </span>
<p className="text-grey-40 inter-small-regular text-center leading-6"> <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 /> <br />
merchants and e-commerce developers {t(
"help-dialog-merchants-and-e-commerce-developers",
"merchants and e-commerce developers"
)}
</p> </p>
</div> </div>
</a> </a>
<a className="w-full" href={link}> <a className="w-full" href={link}>
<Button variant="primary" size="large" className="w-full"> <Button variant="primary" size="large" className="w-full">
Send a message {t("help-dialog-send-a-message", "Send a message")}
</Button> </Button>
</a> </a>
</div> </div>
@@ -1,6 +1,7 @@
import { useAdminCreateInvite } from "medusa-react" import { useAdminCreateInvite } from "medusa-react"
import React from "react" import React from "react"
import { Controller, useForm } from "react-hook-form" import { Controller, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { Role } from "../../../types/shared" import { Role } from "../../../types/shared"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
@@ -20,6 +21,7 @@ type InviteModalFormData = {
const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => { const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
const notification = useNotification() const notification = useNotification()
const { t } = useTranslation()
const { mutate, isLoading } = useAdminCreateInvite() const { mutate, isLoading } = useAdminCreateInvite()
@@ -33,20 +35,34 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
}, },
{ {
onSuccess: () => { 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() handleClose()
}, },
onError: (error) => { onError: (error) => {
notification("Error", getErrorMessage(error), "error") notification(
t("invite-modal-error", "Error"),
getErrorMessage(error),
"error"
)
}, },
} }
) )
} }
const roleOptions: Role[] = [ const roleOptions: Role[] = [
{ value: "member", label: "Member" }, { value: "member", label: t("invite-modal-member", "Member") },
{ value: "admin", label: "Admin" }, { value: "admin", label: t("invite-modal-admin", "Admin") },
{ value: "developer", label: "Developer" }, { value: "developer", label: t("invite-modal-developer", "Developer") },
] ]
return ( return (
@@ -54,12 +70,14 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={handleClose}> <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.Header>
<Modal.Content> <Modal.Content>
<div className="gap-y-base flex flex-col"> <div className="gap-y-base flex flex-col">
<InputField <InputField
label="Email" label={t("invite-modal-email", "Email")}
placeholder="lebron@james.com" placeholder="lebron@james.com"
required required
{...register("user", { required: true })} {...register("user", { required: true })}
@@ -67,12 +85,15 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
<Controller <Controller
name="role" name="role"
control={control} control={control}
defaultValue={{ label: "Member", value: "member" }} defaultValue={{
label: t("invite-modal-member", "Member"),
value: "member",
}}
render={({ field: { value, onChange, onBlur, ref } }) => { render={({ field: { value, onChange, onBlur, ref } }) => {
return ( return (
<NextSelect <NextSelect
label="Role" label={t("invite-modal-role", "Role")}
placeholder="Select role" placeholder={t("invite-modal-select-role", "Select role")}
onBlur={onBlur} onBlur={onBlur}
ref={ref} ref={ref}
onChange={onChange} onChange={onChange}
@@ -93,7 +114,7 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
type="button" type="button"
onClick={handleClose} onClick={handleClose}
> >
Cancel {t("invite-modal-cancel", "Cancel")}
</Button> </Button>
<Button <Button
loading={isLoading} loading={isLoading}
@@ -102,7 +123,7 @@ const InviteModal: React.FC<InviteModalProps> = ({ handleClose }) => {
className="text-small w-32 justify-center" className="text-small w-32 justify-center"
variant="primary" variant="primary"
> >
Invite {t("invite-modal-invite", "Invite")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -2,6 +2,7 @@ import { useAdminLogin } from "medusa-react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { useWidgets } from "../../../providers/widget-provider" import { useWidgets } from "../../../providers/widget-provider"
import { useTranslation } from "react-i18next"
import InputError from "../../atoms/input-error" import InputError from "../../atoms/input-error"
import WidgetContainer from "../../extensions/widget-container" import WidgetContainer from "../../extensions/widget-container"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
@@ -25,6 +26,7 @@ const LoginCard = ({ toResetPassword }: LoginCardProps) => {
} = useForm<FormValues>() } = useForm<FormValues>()
const navigate = useNavigate() const navigate = useNavigate()
const { mutate, isLoading } = useAdminLogin() const { mutate, isLoading } = useAdminLogin()
const { t } = useTranslation()
const { getWidgets } = useWidgets() const { getWidgets } = useWidgets()
@@ -38,7 +40,10 @@ const LoginCard = ({ toResetPassword }: LoginCardProps) => {
"password", "password",
{ {
type: "manual", 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, shouldFocus: true,
@@ -62,17 +67,17 @@ const LoginCard = ({ toResetPassword }: LoginCardProps) => {
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<h1 className="inter-xlarge-semibold text-grey-90 mb-large text-[20px]"> <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> </h1>
<div> <div>
<SigninInput <SigninInput
placeholder="Email" placeholder={t("login-card-email", "Email")}
{...register("email", { required: true })} {...register("email", { required: true })}
autoComplete="email" autoComplete="email"
className="mb-small" className="mb-small"
/> />
<SigninInput <SigninInput
placeholder="Password" placeholder={t("login-card-password", "Password")}
type={"password"} type={"password"}
{...register("password", { required: true })} {...register("password", { required: true })}
autoComplete="current-password" autoComplete="current-password"
@@ -93,7 +98,7 @@ const LoginCard = ({ toResetPassword }: LoginCardProps) => {
className="inter-small-regular text-grey-50 mt-8 cursor-pointer" className="inter-small-regular text-grey-50 mt-8 cursor-pointer"
onClick={toResetPassword} onClick={toResetPassword}
> >
Forgot your password? {t("login-card-forgot-your-password", "Forgot your password?")}
</span> </span>
</div> </div>
</form> </form>
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import PlusIcon from "../../fundamentals/icons/plus-icon" import PlusIcon from "../../fundamentals/icons/plus-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon" import TrashIcon from "../../fundamentals/icons/trash-icon"
@@ -20,6 +21,7 @@ const Metadata: React.FC<AddMetadataProps> = ({
setMetadata, setMetadata,
heading = "Metadata", heading = "Metadata",
}) => { }) => {
const { t } = useTranslation()
const [localData, setLocalData] = useState<MetadataField[]>([]) const [localData, setLocalData] = useState<MetadataField[]>([])
useEffect(() => { useEffect(() => {
@@ -79,7 +81,7 @@ const Metadata: React.FC<AddMetadataProps> = ({
onClick={addKeyPair} onClick={addKeyPair}
> >
<PlusIcon size={20} /> <PlusIcon size={20} />
Add Metadata {t("metadata-add-metadata", "Add Metadata")}
</Button> </Button>
</div> </div>
</div> </div>
@@ -1,6 +1,7 @@
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { useEffect } from "react" import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../hooks/use-edit-product-actions" import useEditProductActions from "../../../hooks/use-edit-product-actions"
import { countries } from "../../../utils/countries" import { countries } from "../../../utils/countries"
import { nestedForm } from "../../../utils/nested-form" import { nestedForm } from "../../../utils/nested-form"
@@ -23,6 +24,7 @@ type AttributesForm = {
} }
const AttributeModal = ({ product, open, onClose }: Props) => { const AttributeModal = ({ product, open, onClose }: Props) => {
const { t } = useTranslation()
const { onUpdate, updating } = useEditProductActions(product.id) const { onUpdate, updating } = useEditProductActions(product.id)
const form = useForm<AttributesForm>({ const form = useForm<AttributesForm>({
defaultValues: getDefaultValues(product), defaultValues: getDefaultValues(product),
@@ -67,21 +69,33 @@ const AttributeModal = ({ product, open, onClose }: Props) => {
<Modal open={open} handleClose={onReset} isLargeModal> <Modal open={open} handleClose={onReset} isLargeModal>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={onReset}> <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> </Modal.Header>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Modal.Content> <Modal.Content>
<div className="mb-xlarge"> <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"> <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> </p>
<DimensionsForm form={nestedForm(form, "dimensions")} /> <DimensionsForm form={nestedForm(form, "dimensions")} />
</div> </div>
<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"> <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> </p>
<CustomsForm form={nestedForm(form, "customs")} /> <CustomsForm form={nestedForm(form, "customs")} />
</div> </div>
@@ -94,7 +108,7 @@ const AttributeModal = ({ product, open, onClose }: Props) => {
type="button" type="button"
onClick={onReset} onClick={onReset}
> >
Cancel {t("product-attributes-section-cancel", "Cancel")}
</Button> </Button>
<Button <Button
size="small" size="small"
@@ -103,7 +117,7 @@ const AttributeModal = ({ product, open, onClose }: Props) => {
disabled={!isDirty} disabled={!isDirty}
loading={updating} loading={updating}
> >
Save {t("product-attributes-section-save", "Save")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,4 +1,5 @@
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import useToggleState from "../../../hooks/use-toggle-state" import useToggleState from "../../../hooks/use-toggle-state"
import EditIcon from "../../fundamentals/icons/edit-icon" import EditIcon from "../../fundamentals/icons/edit-icon"
import { ActionType } from "../../molecules/actionables" import { ActionType } from "../../molecules/actionables"
@@ -10,6 +11,7 @@ type Props = {
} }
const ProductAttributesSection = ({ product }: Props) => { const ProductAttributesSection = ({ product }: Props) => {
const { t } = useTranslation()
const { state, toggle, close } = useToggleState() const { state, toggle, close } = useToggleState()
const actions: ActionType[] = [ const actions: ActionType[] = [
@@ -24,7 +26,9 @@ const ProductAttributesSection = ({ product }: Props) => {
<> <>
<Section title="Attributes" actions={actions} forceDropdown> <Section title="Attributes" actions={actions} forceDropdown>
<div className="gap-y-xsmall mb-large mt-base flex flex-col"> <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"> <div className="gap-y-xsmall flex flex-col">
<Attribute attribute="Height" value={product.height} /> <Attribute attribute="Height" value={product.height} />
<Attribute attribute="Width" value={product.width} /> <Attribute attribute="Width" value={product.width} />
@@ -33,12 +37,23 @@ const ProductAttributesSection = ({ product }: Props) => {
</div> </div>
</div> </div>
<div className="gap-y-xsmall flex flex-col"> <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"> <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
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} value={product.origin_country}
/> />
</div> </div>
@@ -1,5 +1,6 @@
import { Product, SalesChannel } from "@medusajs/medusa" import { Product, SalesChannel } from "@medusajs/medusa"
import { useAdminUpdateProduct } from "medusa-react" import { useAdminUpdateProduct } from "medusa-react"
import { useTranslation } from "react-i18next"
import SalesChannelsModal from "../../forms/product/sales-channels-modal" import SalesChannelsModal from "../../forms/product/sales-channels-modal"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
@@ -11,6 +12,7 @@ type Props = {
const ChannelsModal = ({ product, open, onClose }: Props) => { const ChannelsModal = ({ product, open, onClose }: Props) => {
const notification = useNotification() const notification = useNotification()
const { t } = useTranslation()
const { mutateAsync } = useAdminUpdateProduct(product.id) const { mutateAsync } = useAdminUpdateProduct(product.id)
@@ -19,9 +21,23 @@ const ChannelsModal = ({ product, open, onClose }: Props) => {
await mutateAsync({ await mutateAsync({
sales_channels: channels.map((c) => ({ id: c.id })), 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) { } 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"
)
} }
} }
@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next"
import DiscountableForm, { import DiscountableForm, {
DiscountableFormType, DiscountableFormType,
} from "../../forms/product/discountable-form" } from "../../forms/product/discountable-form"
@@ -33,6 +34,7 @@ type GeneralFormWrapper = {
} }
const GeneralModal = ({ product, open, onClose }: Props) => { const GeneralModal = ({ product, open, onClose }: Props) => {
const { t } = useTranslation()
const { onUpdate, updating } = useEditProductActions(product.id) const { onUpdate, updating } = useEditProductActions(product.id)
const form = useForm<GeneralFormWrapper>({ const form = useForm<GeneralFormWrapper>({
defaultValues: getDefaultValues(product), defaultValues: getDefaultValues(product),
@@ -95,7 +97,10 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={onReset}> <Modal.Header handleClose={onReset}>
<h1 className="inter-xlarge-semibold m-0"> <h1 className="inter-xlarge-semibold m-0">
Edit General Information {t(
"product-general-section-edit-general-information",
"Edit General Information"
)}
</h1> </h1>
</Modal.Header> </Modal.Header>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
@@ -106,7 +111,10 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
/> />
<div className="my-xlarge"> <div className="my-xlarge">
<h2 className="inter-base-semibold mb-base"> <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> </h2>
<OrganizeForm form={nestedForm(form, "organize")} /> <OrganizeForm form={nestedForm(form, "organize")} />
</div> </div>
@@ -115,7 +123,9 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
isGiftCard={product.is_giftcard} isGiftCard={product.is_giftcard}
/> />
<div className="mt-xlarge"> <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")} /> <MetadataForm form={nestedForm(form, "metadata")} />
</div> </div>
</Modal.Content> </Modal.Content>
@@ -127,7 +137,7 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
type="button" type="button"
onClick={onReset} onClick={onReset}
> >
Cancel {t("product-general-section-cancel", "Cancel")}
</Button> </Button>
<Button <Button
size="small" size="small"
@@ -136,7 +146,7 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
disabled={!isDirty} disabled={!isDirty}
loading={updating} loading={updating}
> >
Save {t("product-general-section-save", "Save")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,4 +1,5 @@
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../hooks/use-edit-product-actions" import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useToggleState from "../../../hooks/use-toggle-state" import useToggleState from "../../../hooks/use-toggle-state"
import { import {
@@ -22,6 +23,7 @@ type Props = {
} }
const ProductGeneralSection = ({ product }: Props) => { const ProductGeneralSection = ({ product }: Props) => {
const { t } = useTranslation()
const { onDelete, onStatusChange } = useEditProductActions(product.id) const { onDelete, onStatusChange } = useEditProductActions(product.id)
const { const {
state: infoState, state: infoState,
@@ -39,12 +41,15 @@ const ProductGeneralSection = ({ product }: Props) => {
const actions: ActionType[] = [ const actions: ActionType[] = [
{ {
label: "Edit General Information", label: t(
"product-general-section-edit-general-information",
"Edit General Information"
),
onClick: toggleInfo, onClick: toggleInfo,
icon: <EditIcon size={20} />, icon: <EditIcon size={20} />,
}, },
{ {
label: "Delete", label: t("product-general-section-delete", "Delete"),
onClick: onDelete, onClick: onDelete,
variant: "danger", variant: "danger",
icon: <TrashIcon size={20} />, icon: <TrashIcon size={20} />,
@@ -53,7 +58,10 @@ const ProductGeneralSection = ({ product }: Props) => {
if (isFeatureEnabled("sales_channels")) { if (isFeatureEnabled("sales_channels")) {
actions.splice(1, 0, { actions.splice(1, 0, {
label: "Edit Sales Channels", label: t(
"product-general-section-edit-sales-channels",
"Edit Sales Channels"
),
onClick: toggleChannels, onClick: toggleChannels,
icon: <ChannelsIcon size={20} />, icon: <ChannelsIcon size={20} />,
}) })
@@ -68,8 +76,8 @@ const ProductGeneralSection = ({ product }: Props) => {
status={ status={
<StatusSelector <StatusSelector
isDraft={product?.status === "draft"} isDraft={product?.status === "draft"}
activeState="Published" activeState={t("product-general-section-published", "Published")}
draftState="Draft" draftState={t("product-general-section-draft", "Draft")}
onChange={() => onStatusChange(product.status)} onChange={() => onStatusChange(product.status)}
/> />
} }
@@ -123,33 +131,50 @@ const Detail = ({ title, value }: DetailProps) => {
const ProductDetails = ({ product }: Props) => { const ProductDetails = ({ product }: Props) => {
const { isFeatureEnabled } = useFeatureFlag() const { isFeatureEnabled } = useFeatureFlag()
const { t } = useTranslation()
return ( return (
<div className="mt-8 flex flex-col gap-y-3"> <div className="mt-8 flex flex-col gap-y-3">
<h2 className="inter-base-semibold">Details</h2> <h2 className="inter-base-semibold">
<Detail title="Subtitle" value={product.subtitle} /> {t("product-general-section-details", "Details")}
<Detail title="Handle" value={product.handle} /> </h2>
<Detail title="Type" value={product.type?.value} /> <Detail
<Detail title="Collection" value={product.collection?.title} /> 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) && ( {isFeatureEnabled(FeatureFlag.PRODUCT_CATEGORIES) && (
<Detail <Detail
title="Category" title={t("product-general-section-category", "Category")}
value={product.categories.map((c) => c.name)} value={product.categories.map((c) => c.name)}
/> />
)} )}
<Detail <Detail
title="Discountable" title={t("product-general-section-discountable", "Discountable")}
value={product.discountable ? "True" : "False"} value={
product.discountable
? t("product-general-section-true", "True")
: t("product-general-section-false", "False")
}
/> />
<Detail <Detail
title="Metadata" title={t("product-general-section-metadata", "Metadata")}
value={ value={
Object.entries(product.metadata || {}).length > 0 Object.entries(product.metadata || {}).length > 0
? `${Object.entries(product.metadata || {}).length} ${ ? t("product-general-section-count", "{{count}}", {
Object.keys(product.metadata || {}).length === 1 count: Object.keys(product.metadata || {}).length,
? "item" })
: "items"
}`
: undefined : undefined
} }
/> />
@@ -176,10 +201,13 @@ const ProductTags = ({ product }: Props) => {
} }
const ProductSalesChannels = ({ product }: Props) => { const ProductSalesChannels = ({ product }: Props) => {
const { t } = useTranslation()
return ( return (
<FeatureToggle featureFlag="sales_channels"> <FeatureToggle featureFlag="sales_channels">
<div className="mt-xlarge"> <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} /> <SalesChannelsDisplay channels={product.sales_channels} />
</div> </div>
</FeatureToggle> </FeatureToggle>
@@ -1,4 +1,5 @@
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import useToggleState from "../../../hooks/use-toggle-state" import useToggleState from "../../../hooks/use-toggle-state"
import { ActionType } from "../../molecules/actionables" import { ActionType } from "../../molecules/actionables"
import Section from "../../organisms/section" import Section from "../../organisms/section"
@@ -10,10 +11,11 @@ type Props = {
const ProductMediaSection = ({ product }: Props) => { const ProductMediaSection = ({ product }: Props) => {
const { state, close, toggle } = useToggleState() const { state, close, toggle } = useToggleState()
const { t } = useTranslation()
const actions: ActionType[] = [ const actions: ActionType[] = [
{ {
label: "Edit Media", label: t("product-media-section-edit-media", "Edit Media"),
onClick: toggle, onClick: toggle,
}, },
] ]
@@ -1,6 +1,7 @@
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { useEffect } from "react" import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../hooks/use-edit-product-actions" import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { FormImage } from "../../../types/shared" import { FormImage } from "../../../types/shared"
@@ -21,6 +22,7 @@ type MediaFormWrapper = {
} }
const MediaModal = ({ product, open, onClose }: Props) => { const MediaModal = ({ product, open, onClose }: Props) => {
const { t } = useTranslation()
const { onUpdate, updating } = useEditProductActions(product.id) const { onUpdate, updating } = useEditProductActions(product.id)
const form = useForm<MediaFormWrapper>({ const form = useForm<MediaFormWrapper>({
defaultValues: getDefaultValues(product), defaultValues: getDefaultValues(product),
@@ -49,17 +51,27 @@ const MediaModal = ({ product, open, onClose }: Props) => {
try { try {
preppedImages = await prepareImages(data.media.images) preppedImages = await prepareImages(data.media.images)
} catch (error) { } 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 const response = (error as any).response as Response
if (response.status === 500) { if (response.status === 500) {
errorMessage = errorMessage =
errorMessage + errorMessage +
" " + " " +
t(
"product-media-section-file-service-not-configured",
"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-media-section-error", "Error"),
errorMessage,
"error"
)
return return
} }
const urls = preppedImages.map((image) => image.url) const urls = preppedImages.map((image) => image.url)
@@ -76,14 +88,21 @@ const MediaModal = ({ product, open, onClose }: Props) => {
<Modal open={open} handleClose={onReset} isLargeModal> <Modal open={open} handleClose={onReset} isLargeModal>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={onReset}> <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> </Modal.Header>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Modal.Content> <Modal.Content>
<div> <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"> <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> </p>
<div> <div>
<MediaForm form={nestedForm(form, "media")} /> <MediaForm form={nestedForm(form, "media")} />
@@ -98,7 +117,7 @@ const MediaModal = ({ product, open, onClose }: Props) => {
type="button" type="button"
onClick={onReset} onClick={onReset}
> >
Cancel {t("product-media-section-cancel", "Cancel")}
</Button> </Button>
<Button <Button
size="small" size="small"
@@ -107,7 +126,7 @@ const MediaModal = ({ product, open, onClose }: Props) => {
disabled={!isDirty} disabled={!isDirty}
loading={updating} loading={updating}
> >
Save and close {t("product-media-section-save-and-close", "Save and close")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,4 +1,5 @@
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import JSONView from "../../molecules/json-view" import JSONView from "../../molecules/json-view"
import Section from "../section" 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. */ /** 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 ProductRawSection = ({ product }: Props) => {
const { t } = useTranslation()
return ( 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"> <div className="pt-base">
<JSONView data={product} /> <JSONView data={product} />
</div> </div>
@@ -1,5 +1,6 @@
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import clsx from "clsx" import clsx from "clsx"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../hooks/use-edit-product-actions" import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import useToggleState from "../../../hooks/use-toggle-state" import useToggleState from "../../../hooks/use-toggle-state"
@@ -14,6 +15,7 @@ type Props = {
} }
const ProductThumbnailSection = ({ product }: Props) => { const ProductThumbnailSection = ({ product }: Props) => {
const { t } = useTranslation()
const { onUpdate, updating } = useEditProductActions(product.id) const { onUpdate, updating } = useEditProductActions(product.id)
const { state, toggle, close } = useToggleState() const { state, toggle, close } = useToggleState()
@@ -27,10 +29,21 @@ const ProductThumbnailSection = ({ product }: Props) => {
}, },
{ {
onSuccess: () => { 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) => { 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" type="button"
onClick={toggle} onClick={toggle}
> >
{product.thumbnail ? "Edit" : "Upload"} {product.thumbnail
? t("product-thumbnail-section-edit", "Edit")
: t("product-thumbnail-section-upload", "Upload")}
</Button> </Button>
{product.thumbnail && ( {product.thumbnail && (
<TwoStepDelete onDelete={handleDelete} deleting={updating} /> <TwoStepDelete onDelete={handleDelete} deleting={updating} />
@@ -1,6 +1,7 @@
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { useEffect } from "react" import { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../hooks/use-edit-product-actions" import useEditProductActions from "../../../hooks/use-edit-product-actions"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { FormImage } from "../../../types/shared" import { FormImage } from "../../../types/shared"
@@ -23,6 +24,7 @@ type ThumbnailFormWrapper = {
} }
const ThumbnailModal = ({ product, open, onClose }: Props) => { const ThumbnailModal = ({ product, open, onClose }: Props) => {
const { t } = useTranslation()
const { onUpdate, updating } = useEditProductActions(product.id) const { onUpdate, updating } = useEditProductActions(product.id)
const form = useForm<ThumbnailFormWrapper>({ const form = useForm<ThumbnailFormWrapper>({
defaultValues: getDefaultValues(product), defaultValues: getDefaultValues(product),
@@ -51,18 +53,27 @@ const ThumbnailModal = ({ product, open, onClose }: Props) => {
try { try {
preppedImages = await prepareImages(data.thumbnail.images) preppedImages = await prepareImages(data.thumbnail.images)
} catch (error) { } catch (error) {
let errorMessage = let errorMessage = t(
"product-thumbnail-section-upload-thumbnail-error",
"Something went wrong while trying to upload the thumbnail." "Something went wrong while trying to upload the thumbnail."
)
const response = (error as any).response as Response const response = (error as any).response as Response
if (response.status === 500) { if (response.status === 500) {
errorMessage = errorMessage =
errorMessage + errorMessage +
" " + " " +
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" "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 return
} }
const url = preppedImages?.[0]?.url const url = preppedImages?.[0]?.url
@@ -80,14 +91,23 @@ const ThumbnailModal = ({ product, open, onClose }: Props) => {
<Modal open={open} handleClose={onReset} isLargeModal> <Modal open={open} handleClose={onReset} isLargeModal>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={onReset}> <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> </Modal.Header>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Modal.Content> <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"> <p className="inter-base-regular text-grey-50 mb-large">
Used to represent your product during checkout, social sharing and {t(
more. "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> </p>
<ThumbnailForm form={nestedForm(form, "thumbnail")} /> <ThumbnailForm form={nestedForm(form, "thumbnail")} />
</Modal.Content> </Modal.Content>
@@ -99,7 +119,7 @@ const ThumbnailModal = ({ product, open, onClose }: Props) => {
type="button" type="button"
onClick={onReset} onClick={onReset}
> >
Cancel {t("product-thumbnail-section-cancel", "Cancel")}
</Button> </Button>
<Button <Button
size="small" size="small"
@@ -108,7 +128,10 @@ const ThumbnailModal = ({ product, open, onClose }: Props) => {
disabled={!isDirty} disabled={!isDirty}
loading={updating} loading={updating}
> >
Save and close {t(
"product-thumbnail-section-save-and-close",
"Save and close"
)}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,5 +1,6 @@
import { Product, ProductVariant } from "@medusajs/medusa" import { Product, ProductVariant } from "@medusajs/medusa"
import React from "react" import React from "react"
import { useTranslation } from "react-i18next"
import { ActionType } from "../../molecules/actionables" import { ActionType } from "../../molecules/actionables"
import { CollapsibleTree } from "../../molecules/collapsible-tree" import { CollapsibleTree } from "../../molecules/collapsible-tree"
@@ -58,6 +59,7 @@ const ProductVariantTree: React.FC<ProductVariantTreeProps> = ({
} }
const ProductVariantLeaf = ({ sku, title, prices = [] }: LeafProps) => { const ProductVariantLeaf = ({ sku, title, prices = [] }: LeafProps) => {
const { t } = useTranslation()
const filteredPrices = prices.filter((pr) => pr.price_list_id) const filteredPrices = prices.filter((pr) => pr.price_list_id)
return ( return (
<div className="flex flex-1"> <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 flex flex-1 items-center justify-end">
<div className="text-grey-50 mr-xsmall"> <div className="text-grey-50 mr-xsmall">
{filteredPrices.length ? ( {filteredPrices.length ? (
<span>{`${filteredPrices.length} price${ <span>
filteredPrices.length > 1 ? "s" : "" {t("product-variant-tree-count", "{{count}}", {
}`}</span> count: filteredPrices.length,
})}
</span>
) : ( ) : (
<span className="inter-small-semibold text-orange-40"> <span className="inter-small-semibold text-orange-40">
Add prices {t("product-variant-tree-add-prices", "Add prices")}
</span> </span>
)} )}
</div> </div>
@@ -1,5 +1,6 @@
import { AdminPostProductsProductVariantsReq, Product } from "@medusajs/medusa" import { AdminPostProductsProductVariantsReq, Product } from "@medusajs/medusa"
import { useEffect } from "react" import { useEffect } from "react"
import { useTranslation } from "react-i18next"
import EditFlowVariantForm, { import EditFlowVariantForm, {
EditFlowVariantFormType, EditFlowVariantFormType,
} from "../../forms/product/variant-form/edit-flow-variant-form" } from "../../forms/product/variant-form/edit-flow-variant-form"
@@ -22,6 +23,7 @@ type Props = {
const AddVariantModal = ({ open, onClose, product }: Props) => { const AddVariantModal = ({ open, onClose, product }: Props) => {
const context = useLayeredModal() const context = useLayeredModal()
const { t } = useTranslation()
const { client } = useMedusa() const { client } = useMedusa()
const form = useForm<EditFlowVariantFormType>({ const form = useForm<EditFlowVariantFormType>({
@@ -93,7 +95,9 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
<LayeredModal context={context} open={open} handleClose={resetAndClose}> <LayeredModal context={context} open={open} handleClose={resetAndClose}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={resetAndClose}> <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> </Modal.Header>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Modal.Content> <Modal.Content>
@@ -107,7 +111,7 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
type="button" type="button"
onClick={resetAndClose} onClick={resetAndClose}
> >
Cancel {t("product-variants-section-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
@@ -115,7 +119,7 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
type="submit" type="submit"
loading={addingVariant} loading={addingVariant}
> >
Save and close {t("product-variants-section-save-and-close", "Save and close")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next"
import EditFlowVariantForm, { import EditFlowVariantForm, {
EditFlowVariantFormType, EditFlowVariantFormType,
} from "../../forms/product/variant-inventory-form/edit-flow-variant-form" } from "../../forms/product/variant-inventory-form/edit-flow-variant-form"
@@ -30,6 +31,7 @@ type Props = {
} }
const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => { const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
const { t } = useTranslation()
const { client } = useMedusa() const { client } = useMedusa()
const layeredModalContext = useContext(LayeredModalContext) const layeredModalContext = useContext(LayeredModalContext)
const { const {
@@ -191,7 +193,12 @@ const EditVariantInventoryModal = ({ onClose, product, variant }: Props) => {
return ( return (
<LayeredModal context={layeredModalContext} handleClose={handleClose}> <LayeredModal context={layeredModalContext} handleClose={handleClose}>
<Modal.Header 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> </Modal.Header>
{!isLoadingInventory && ( {!isLoadingInventory && (
<StockForm <StockForm
@@ -222,6 +229,7 @@ const StockForm = ({
handleClose: () => void handleClose: () => void
updatingVariant: boolean updatingVariant: boolean
}) => { }) => {
const { t } = useTranslation()
const form = useForm<EditFlowVariantFormType>({ const form = useForm<EditFlowVariantFormType>({
// @ts-ignore // @ts-ignore
defaultValues: getEditVariantDefaultValues(variantInventory, variant), defaultValues: getEditVariantDefaultValues(variantInventory, variant),
@@ -259,7 +267,7 @@ const StockForm = ({
handleClose() handleClose()
}} }}
> >
Cancel {t("product-variants-section-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
@@ -268,7 +276,7 @@ const StockForm = ({
disabled={!isDirty} disabled={!isDirty}
loading={updatingVariant} loading={updatingVariant}
> >
Save and close {t("product-variants-section-save-and-close", "Save and close")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,4 +1,5 @@
import { Product, ProductVariant } from "@medusajs/medusa" import { Product, ProductVariant } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import EditFlowVariantForm, { import EditFlowVariantForm, {
EditFlowVariantFormType, EditFlowVariantFormType,
} from "../../forms/product/variant-form/edit-flow-variant-form" } from "../../forms/product/variant-form/edit-flow-variant-form"
@@ -30,6 +31,7 @@ const EditVariantModal = ({
variant, variant,
isDuplicate = false, isDuplicate = false,
}: Props) => { }: Props) => {
const { t } = useTranslation()
const form = useForm<EditFlowVariantFormType>({ const form = useForm<EditFlowVariantFormType>({
// @ts-ignore // @ts-ignore
defaultValues: getEditVariantDefaultValues(variant, product), defaultValues: getEditVariantDefaultValues(variant, product),
@@ -111,7 +113,7 @@ const EditVariantModal = ({
<LayeredModal context={layeredModalContext} handleClose={handleClose}> <LayeredModal context={layeredModalContext} handleClose={handleClose}>
<Modal.Header handleClose={handleClose}> <Modal.Header handleClose={handleClose}>
<h1 className="inter-xlarge-semibold"> <h1 className="inter-xlarge-semibold">
Edit Variant {t("product-variants-section-edit-variant", "Edit Variant")}
{variant.title && ( {variant.title && (
<span className="inter-xlarge-regular text-grey-50"> <span className="inter-xlarge-regular text-grey-50">
{" "} {" "}
@@ -132,7 +134,7 @@ const EditVariantModal = ({
type="button" type="button"
onClick={handleClose} onClick={handleClose}
> >
Cancel {t("product-variants-section-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
@@ -141,7 +143,7 @@ const EditVariantModal = ({
disabled={!isDirty && !isDuplicate} disabled={!isDirty && !isDuplicate}
loading={addingVariant || updatingVariant} loading={addingVariant || updatingVariant}
> >
Save and close {t("product-variants-section-save-and-close", "Save and close")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -4,6 +4,7 @@ import {
ProductVariant, ProductVariant,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import React, { useContext, useEffect, useMemo } from "react" import React, { useContext, useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import EditFlowVariantForm, { import EditFlowVariantForm, {
EditFlowVariantFormType, EditFlowVariantFormType,
} from "../../../forms/product/variant-form/edit-flow-variant-form" } from "../../../forms/product/variant-form/edit-flow-variant-form"
@@ -23,6 +24,7 @@ type Props = {
} }
const EditVariantScreen = ({ variant, product }: Props) => { const EditVariantScreen = ({ variant, product }: Props) => {
const { t } = useTranslation()
const { onClose } = useEditVariantsModal() const { onClose } = useEditVariantsModal()
const form = useForm<EditFlowVariantFormType>({ const form = useForm<EditFlowVariantFormType>({
defaultValues: getEditVariantDefaultValues(variant, product), defaultValues: getEditVariantDefaultValues(variant, product),
@@ -67,7 +69,7 @@ const EditVariantScreen = ({ variant, product }: Props) => {
<Modal.Footer> <Modal.Footer>
<div className="gap-x-xsmall flex w-full items-center justify-end"> <div className="gap-x-xsmall flex w-full items-center justify-end">
<Button variant="secondary" size="small" type="button"> <Button variant="secondary" size="small" type="button">
Cancel {t("edit-variants-modal-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
@@ -77,7 +79,7 @@ const EditVariantScreen = ({ variant, product }: Props) => {
loading={updatingVariant} loading={updatingVariant}
onClick={onSubmitAndBack} onClick={onSubmitAndBack}
> >
Save and go back {t("edit-variants-modal-save-and-go-back", "Save and go back")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
@@ -87,7 +89,7 @@ const EditVariantScreen = ({ variant, product }: Props) => {
loading={updatingVariant} loading={updatingVariant}
onClick={onSubmitAndClose} onClick={onSubmitAndClose}
> >
Save and close {t("edit-variants-modal-save-and-close", "Save and close")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -133,11 +135,12 @@ export const createUpdatePayload = (
} }
export const useEditVariantScreen = (props: Props) => { export const useEditVariantScreen = (props: Props) => {
const { t } = useTranslation()
const { pop } = React.useContext(LayeredModalContext) const { pop } = React.useContext(LayeredModalContext)
const screen = useMemo(() => { const screen = useMemo(() => {
return { return {
title: "Edit Variant", title: t("edit-variants-modal-edit-variant", "Edit Variant"),
subtitle: props.variant.title, subtitle: props.variant.title,
onBack: pop, onBack: pop,
view: <EditVariantScreen {...props} />, view: <EditVariantScreen {...props} />,
@@ -6,6 +6,7 @@ import {
useFieldArray, useFieldArray,
useForm, useForm,
} from "react-hook-form" } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useEditProductActions from "../../../../hooks/use-edit-product-actions" import useEditProductActions from "../../../../hooks/use-edit-product-actions"
import Button from "../../../fundamentals/button" import Button from "../../../fundamentals/button"
import Modal from "../../../molecules/modal" import Modal from "../../../molecules/modal"
@@ -36,6 +37,7 @@ export type EditVariantsForm = {
const EditVariantsModal = ({ open, onClose, product }: Props) => { const EditVariantsModal = ({ open, onClose, product }: Props) => {
const context = useContext(LayeredModalContext) const context = useContext(LayeredModalContext)
const { onUpdate, updating } = useEditProductActions(product.id) const { onUpdate, updating } = useEditProductActions(product.id)
const { t } = useTranslation()
const form = useForm<EditVariantsForm>({ const form = useForm<EditVariantsForm>({
defaultValues: getDefaultValues(product), defaultValues: getDefaultValues(product),
@@ -109,8 +111,11 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
() => { () => {
resetAndClose() resetAndClose()
}, },
t(
"edit-variants-modal-update-success",
"Variants were successfully updated" "Variants were successfully updated"
) )
)
}) })
return ( return (
@@ -122,20 +127,29 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
<LayeredModal handleClose={resetAndClose} open={open} context={context}> <LayeredModal handleClose={resetAndClose} open={open} context={context}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={resetAndClose}> <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> </Modal.Header>
<FormProvider {...form}> <FormProvider {...form}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Modal.Content> <Modal.Content>
<h2 className="inter-base-semibold mb-small"> <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"> <span className="inter-base-regular text-grey-50">
({product.variants.length}) ({product.variants.length})
</span> </span>
</h2> </h2>
<div className="pr-base inter-small-semibold text-grey-50 mb-small grid grid-cols-[1fr_1fr_48px]"> <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-1 col-end-1 text-left">
<p className="col-start-2 col-end-2 text-right">Inventory</p> {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>
<div>{fields.map((card, i) => renderCard(card, i))}</div> <div>{fields.map((card, i) => renderCard(card, i))}</div>
</Modal.Content> </Modal.Content>
@@ -147,7 +161,7 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
type="button" type="button"
onClick={resetAndClose} onClick={resetAndClose}
> >
Cancel {t("edit-variants-modal-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
@@ -156,7 +170,7 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
loading={updating} loading={updating}
disabled={updating || !isDirty} disabled={updating || !isDirty}
> >
Save and close {t("edit-variants-modal-save-and-close", "Save and close")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -4,6 +4,7 @@ import type { Identifier, XYCoord } from "dnd-core"
import { useContext, useMemo, useRef } from "react" import { useContext, useMemo, useRef } from "react"
import { useDrag, useDrop } from "react-dnd" import { useDrag, useDrop } from "react-dnd"
import { useFormContext } from "react-hook-form" import { useFormContext } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { VariantItem } from "." import { VariantItem } from "."
import { DragItem } from "../../../../types/shared" import { DragItem } from "../../../../types/shared"
import FormValidator from "../../../../utils/form-validator" import FormValidator from "../../../../utils/form-validator"
@@ -35,6 +36,7 @@ export const VariantCard = ({
moveCard, moveCard,
product, product,
}: VariantCardProps) => { }: VariantCardProps) => {
const { t } = useTranslation()
const { const {
register, register,
formState: { errors }, formState: { errors },
@@ -49,7 +51,7 @@ export const VariantCard = ({
const actions: ActionType[] = useMemo(() => { const actions: ActionType[] = useMemo(() => {
return [ return [
{ {
label: "Edit Variant", label: t("edit-variants-modal-edit-variant", "Edit Variant"),
icon: <EditIcon size={20} className="text-grey-50" />, icon: <EditIcon size={20} className="text-grey-50" />,
onClick: () => push(editVariantScreen), onClick: () => push(editVariantScreen),
}, },
@@ -1,5 +1,6 @@
import OptionsProvider, { useOptionsContext } from "./options-provider" import OptionsProvider, { useOptionsContext } from "./options-provider"
import { Product, ProductVariant, VariantInventory } from "@medusajs/medusa" import { Product, ProductVariant, VariantInventory } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import { ActionType } from "../../molecules/actionables" import { ActionType } from "../../molecules/actionables"
import AddVariantModal from "./add-variant-modal" import AddVariantModal from "./add-variant-modal"
@@ -28,6 +29,7 @@ type Props = {
const ProductVariantsSection = ({ product }: Props) => { const ProductVariantsSection = ({ product }: Props) => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { client } = useMedusa() const { client } = useMedusa()
const { t } = useTranslation()
const { isFeatureEnabled } = useFeatureFlag() const { isFeatureEnabled } = useFeatureFlag()
@@ -69,22 +71,22 @@ const ProductVariantsSection = ({ product }: Props) => {
const actions: ActionType[] = [ const actions: ActionType[] = [
{ {
label: "Add Variant", label: t("product-variants-section-add-variant", "Add Variant"),
onClick: toggleAddVariant, onClick: toggleAddVariant,
icon: <PlusIcon size="20" />, icon: <PlusIcon size="20" />,
}, },
{ {
label: "Edit Prices", label: t("product-variants-section-edit-prices", "Edit Prices"),
onClick: toggleEditPrices, onClick: toggleEditPrices,
icon: <DollarSignIcon size="20" />, icon: <DollarSignIcon size="20" />,
}, },
{ {
label: "Edit Variants", label: t("product-variants-section-edit-variants", "Edit Variants"),
onClick: toggleEditVariants, onClick: toggleEditVariants,
icon: <EditIcon size="20" />, icon: <EditIcon size="20" />,
}, },
{ {
label: "Edit Options", label: t("product-variants-section-edit-options", "Edit Options"),
onClick: toggleOptions, onClick: toggleOptions,
icon: <GearIcon size="20" />, icon: <GearIcon size="20" />,
}, },
@@ -130,7 +132,7 @@ const ProductVariantsSection = ({ product }: Props) => {
<ProductOptions /> <ProductOptions />
<div className="mt-xlarge"> <div className="mt-xlarge">
<h2 className="inter-large-semibold mb-base"> <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"> <span className="inter-large-regular text-grey-50">
({product.variants.length}) ({product.variants.length})
</span> </span>
@@ -6,6 +6,7 @@ import {
} from "medusa-react" } from "medusa-react"
import { useEffect, useMemo } from "react" import { useEffect, useMemo } from "react"
import { useFieldArray, useForm } from "react-hook-form" import { useFieldArray, useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import FormValidator from "../../../utils/form-validator" import FormValidator from "../../../utils/form-validator"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
@@ -41,6 +42,7 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
product.id product.id
) )
const { t } = useTranslation()
const { refetch } = useOptionsContext() const { refetch } = useOptionsContext()
const { const {
@@ -140,7 +142,14 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
}) })
if (errors.length === toCreate.length + toUpdate.length + toDelete.length) { 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 return
} }
@@ -153,7 +162,14 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
} }
refetch() 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() handleClose()
}) })
@@ -161,13 +177,19 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
<Modal open={open} handleClose={handleClose}> <Modal open={open} handleClose={handleClose}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={handleClose}> <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> </Modal.Header>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<Modal.Content> <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"> <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"> <div className="gap-y-xsmall flex flex-col">
{fields.map((field, index) => { {fields.map((field, index) => {
return ( return (
@@ -179,7 +201,10 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
key={field.id} key={field.id}
placeholder="Color" placeholder="Color"
{...register(`options.${index}.title`, { {...register(`options.${index}.title`, {
required: "Option title is required", required: t(
"product-variants-section-option-title-is-required",
"Option title is required"
),
minLength: minLength:
FormValidator.minOneCharRule("Option title"), FormValidator.minOneCharRule("Option title"),
pattern: FormValidator.whiteSpaceRule("Option title"), pattern: FormValidator.whiteSpaceRule("Option title"),
@@ -205,7 +230,8 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
type="button" type="button"
onClick={handleAddAnOption} onClick={handleAddAnOption}
> >
<PlusIcon size="20" /> Add an option <PlusIcon size="20" />{" "}
{t("product-variants-section-add-an-option", "Add an option")}
</Button> </Button>
</Modal.Content> </Modal.Content>
<Modal.Footer> <Modal.Footer>
@@ -216,7 +242,7 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
type="button" type="button"
onClick={handleClose} onClick={handleClose}
> >
Cancel {t("product-variants-section-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
@@ -225,7 +251,7 @@ const OptionsModal = ({ product, open, onClose }: Props) => {
disabled={!isDirty} disabled={!isDirty}
loading={isSubmitting} loading={isSubmitting}
> >
Save and close {t("product-variants-section-save-and-close", "Save and close")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -2,6 +2,7 @@ import { Column, useTable } from "react-table"
import { ProductVariant } from "@medusajs/medusa" import { ProductVariant } from "@medusajs/medusa"
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { useFeatureFlag } from "../../../providers/feature-flag-provider" import { useFeatureFlag } from "../../../providers/feature-flag-provider"
import BuildingsIcon from "../../fundamentals/icons/buildings-icon" import BuildingsIcon from "../../fundamentals/icons/buildings-icon"
import DuplicateIcon from "../../fundamentals/icons/duplicate-icon" import DuplicateIcon from "../../fundamentals/icons/duplicate-icon"
@@ -22,6 +23,7 @@ type Props = {
} }
export const useVariantsTableColumns = (inventoryIsEnabled = false) => { export const useVariantsTableColumns = (inventoryIsEnabled = false) => {
const { t } = useTranslation()
const columns = useMemo<Column<ProductVariant>[]>(() => { const columns = useMemo<Column<ProductVariant>[]>(() => {
const quantityColumns = [] const quantityColumns = []
if (!inventoryIsEnabled) { if (!inventoryIsEnabled) {
@@ -29,7 +31,9 @@ export const useVariantsTableColumns = (inventoryIsEnabled = false) => {
Header: () => { Header: () => {
return ( return (
<div className="text-right"> <div className="text-right">
<span>Inventory</span> <span>
{t("product-variants-section-inventory", "Inventory")}
</span>
</div> </div>
) )
}, },
@@ -47,12 +51,12 @@ export const useVariantsTableColumns = (inventoryIsEnabled = false) => {
} }
return [ return [
{ {
Header: "Title", Header: t("product-variants-section-title", "Title"),
id: "title", id: "title",
accessor: "title", accessor: "title",
}, },
{ {
Header: "SKU", Header: t("product-variants-section-sku", "SKU"),
id: "sku", id: "sku",
accessor: "sku", accessor: "sku",
maxWidth: 264, maxWidth: 264,
@@ -65,7 +69,7 @@ export const useVariantsTableColumns = (inventoryIsEnabled = false) => {
}, },
}, },
{ {
Header: "EAN", Header: t("product-variants-section-ean", "EAN"),
id: "ean", id: "ean",
accessor: "ean", accessor: "ean",
maxWidth: 264, maxWidth: 264,
@@ -85,6 +89,7 @@ export const useVariantsTableColumns = (inventoryIsEnabled = false) => {
} }
const VariantsTable = ({ variants, actions }: Props) => { const VariantsTable = ({ variants, actions }: Props) => {
const { t } = useTranslation()
const { isFeatureEnabled } = useFeatureFlag() const { isFeatureEnabled } = useFeatureFlag()
const hasInventoryService = isFeatureEnabled("inventoryService") const hasInventoryService = isFeatureEnabled("inventoryService")
const columns = useVariantsTableColumns(hasInventoryService) const columns = useVariantsTableColumns(hasInventoryService)
@@ -112,20 +117,26 @@ const VariantsTable = ({ variants, actions }: Props) => {
const inventoryManagementActions = [] const inventoryManagementActions = []
if (hasInventoryService) { if (hasInventoryService) {
inventoryManagementActions.push({ inventoryManagementActions.push({
label: "Manage inventory", label: t(
"product-variants-section-manage-inventory",
"Manage inventory"
),
icon: <BuildingsIcon size="20" />, icon: <BuildingsIcon size="20" />,
onClick: () => updateVariantInventory(variant), onClick: () => updateVariantInventory(variant),
}) })
} }
return [ return [
{ {
label: "Edit Variant", label: t("product-variants-section-edit-variant", "Edit Variant"),
icon: <EditIcon size="20" />, icon: <EditIcon size="20" />,
onClick: () => updateVariant(variant), onClick: () => updateVariant(variant),
}, },
...inventoryManagementActions, ...inventoryManagementActions,
{ {
label: "Duplicate Variant", label: t(
"product-variants-section-duplicate-variant",
"Duplicate Variant"
),
onClick: () => onClick: () =>
// @ts-ignore // @ts-ignore
duplicateVariant({ duplicateVariant({
@@ -135,7 +146,10 @@ const VariantsTable = ({ variants, actions }: Props) => {
icon: <DuplicateIcon size="20" />, icon: <DuplicateIcon size="20" />,
}, },
{ {
label: "Delete Variant", label: t(
"product-variants-section-delete-variant-label",
"Delete Variant"
),
onClick: () => setVariantToRemove(variant), onClick: () => setVariantToRemove(variant),
icon: <TrashIcon size="20" />, icon: <TrashIcon size="20" />,
variant: "danger", variant: "danger",
@@ -185,11 +199,23 @@ const VariantsTable = ({ variants, actions }: Props) => {
<DeletePrompt <DeletePrompt
onDelete={async () => deleteVariant(variantToRemove.id)} onDelete={async () => deleteVariant(variantToRemove.id)}
handleClose={() => setVariantToRemove(null)} handleClose={() => setVariantToRemove(null)}
confirmText="Yes, delete" confirmText={t(
heading="Delete variant" "product-variants-section-yes-delete",
text={`Are you sure you want to delete this variant? ${ "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") 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} successText={false}
@@ -1,6 +1,7 @@
import { useAdminSendResetPasswordToken } from "medusa-react" import { useAdminSendResetPasswordToken } from "medusa-react"
import React, { useState } from "react" import React, { useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
import FormValidator from "../../../utils/form-validator" import FormValidator from "../../../utils/form-validator"
@@ -22,6 +23,7 @@ const emailRegex = new RegExp(
) )
const ResetTokenCard: React.FC<ResetTokenCardProps> = ({ goBack }) => { const ResetTokenCard: React.FC<ResetTokenCardProps> = ({ goBack }) => {
const { t } = useTranslation()
const [mailSent, setSentMail] = useState(false) const [mailSent, setSentMail] = useState(false)
const { const {
register, register,
@@ -42,7 +44,11 @@ const ResetTokenCard: React.FC<ResetTokenCardProps> = ({ goBack }) => {
setSentMail(true) setSentMail(true)
}, },
onError: (error) => { 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}> <form onSubmit={onSubmit}>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<h1 className="inter-xlarge-semibold text-grey-90 mb-xsmall text-[20px]"> <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> </h1>
<span className="inter-base-regular text-grey-50 mb-large text-center"> <span className="inter-base-regular text-grey-50 mb-large text-center">
Enter your email address below, and we&apos;ll <Trans t={t} i18nKey="reset-token-card-password-reset-description">
Enter your email address below, and we'll
<br /> <br />
send you instructions on how to reset send you instructions on how to reset
<br /> <br />
your password. your password.
</Trans>
</span> </span>
{!mailSent ? ( {!mailSent ? (
<> <>
<div className="w-[280px]"> <div className="w-[280px]">
<SigninInput <SigninInput
placeholder="Email" placeholder={t("reset-token-card-email", "Email")}
{...register("email", { {...register("email", {
required: FormValidator.required("Email"), required: FormValidator.required("Email"),
pattern: { pattern: {
value: emailRegex, 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" type="submit"
loading={isLoading} loading={isLoading}
> >
Send reset instructions {t(
"reset-token-card-send-reset-instructions",
"Send reset instructions"
)}
</Button> </Button>
</> </>
) : ( ) : (
@@ -93,7 +107,10 @@ const ResetTokenCard: React.FC<ResetTokenCardProps> = ({ goBack }) => {
</div> </div>
<div className="gap-y-2xsmall flex flex-col"> <div className="gap-y-2xsmall flex flex-col">
<span className="inter-base-regular"> <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> </span>
</div> </div>
</div> </div>
@@ -102,7 +119,7 @@ const ResetTokenCard: React.FC<ResetTokenCardProps> = ({ goBack }) => {
className="inter-small-regular text-grey-50 mt-8 cursor-pointer" className="inter-small-regular text-grey-50 mt-8 cursor-pointer"
onClick={goBack} onClick={goBack}
> >
Go back to sign in {t("reset-token-card-go-back-to-sign-in", "Go back to sign in")}
</span> </span>
</div> </div>
</form> </form>
@@ -1,5 +1,6 @@
import clsx from "clsx" import clsx from "clsx"
import React from "react" import React from "react"
import { useTranslation } from "react-i18next"
import { formatAmountWithSymbol } from "../../../utils/prices" import { formatAmountWithSymbol } from "../../../utils/prices"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import MinusIcon from "../../fundamentals/icons/minus-icon" import MinusIcon from "../../fundamentals/icons/minus-icon"
@@ -39,11 +40,16 @@ const RMAReturnProductsTable: React.FC<RMAReturnProductsTableProps> = ({
handleRemoveItem, handleRemoveItem,
handleToAddQuantity, handleToAddQuantity,
}) => { }) => {
const { t } = useTranslation()
return ( return (
<Table> <Table>
<Table.HeadRow className="text-grey-50 inter-small-semibold"> <Table.HeadRow className="text-grey-50 inter-small-semibold">
<Table.HeadCell>Product Details</Table.HeadCell> <Table.HeadCell>
<Table.HeadCell className="pr-8 text-right">Quantity</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"> <Table.HeadCell className="text-right">
{isAdditionalItems ? "Unit Price" : "Refundable"} {isAdditionalItems ? "Unit Price" : "Refundable"}
</Table.HeadCell> </Table.HeadCell>
@@ -1,6 +1,7 @@
import { LineItem, Order } from "@medusajs/medusa" import { LineItem, Order } from "@medusajs/medusa"
import clsx from "clsx" import clsx from "clsx"
import React, { Fragment, useContext } from "react" import React, { Fragment, useContext } from "react"
import { useTranslation } from "react-i18next"
import RMAReturnReasonSubModal from "../../../domain/orders/details/rma-sub-modals/return-reasons" import RMAReturnReasonSubModal from "../../../domain/orders/details/rma-sub-modals/return-reasons"
import Medusa from "../../../services/api" import Medusa from "../../../services/api"
import { isLineItemCanceled } from "../../../utils/is-line-item" import { isLineItemCanceled } from "../../../utils/is-line-item"
@@ -32,6 +33,7 @@ const RMASelectProductTable: React.FC<RMASelectProductTableProps> = ({
setToReturn, setToReturn,
isSwapOrClaim = false, isSwapOrClaim = false,
}) => { }) => {
const { t } = useTranslation()
const { push, pop } = useContext(LayeredModalContext) const { push, pop } = useContext(LayeredModalContext)
const handleQuantity = (change, item) => { const handleQuantity = (change, item) => {
@@ -112,9 +114,15 @@ const RMASelectProductTable: React.FC<RMASelectProductTableProps> = ({
<Table> <Table>
<Table.Head className="border-none"> <Table.Head className="border-none">
<Table.HeadRow className="text-grey-50 inter-small-semibold"> <Table.HeadRow className="text-grey-50 inter-small-semibold">
<Table.HeadCell colSpan={2}>Product Details</Table.HeadCell> <Table.HeadCell colSpan={2}>
<Table.HeadCell className="pr-8 text-right">Quantity</Table.HeadCell> {t("rma-select-product-table-product-details", "Product Details")}
<Table.HeadCell className="text-right">Refundable</Table.HeadCell> </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.HeadCell></Table.HeadCell>
</Table.HeadRow> </Table.HeadRow>
</Table.Head> </Table.Head>
@@ -231,11 +239,13 @@ const RMASelectProductTable: React.FC<RMASelectProductTableProps> = ({
<span className="ml-2"> <span className="ml-2">
{toReturn[item.id]?.images?.length > 0 && ( {toReturn[item.id]?.images?.length > 0 && (
<> <>
({toReturn[item.id]?.images?.length} image{" "} {t(
{toReturn[item.id]?.images?.length > 1 "rma-select-product-table-images-witch-count",
? "s" "{{count}}",
: ""} {
) count: toReturn[item.id]?.images?.length,
}
)}
</> </>
)} )}
</span> </span>
@@ -263,7 +273,10 @@ const RMASelectProductTable: React.FC<RMASelectProductTableProps> = ({
size="small" size="small"
className="border-grey-20 border" className="border-grey-20 border"
> >
Select Reason {t(
"rma-select-product-table-select-reason",
"Select Reason"
)}
</Button> </Button>
</div> </div>
</Table.Cell> </Table.Cell>
@@ -1,5 +1,6 @@
import { useAdminStore } from "medusa-react" import { useAdminStore } from "medusa-react"
import React, { useState } from "react" import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import { useFeatureFlag } from "../../../providers/feature-flag-provider" import { useFeatureFlag } from "../../../providers/feature-flag-provider"
import { useRoutes } from "../../../providers/route-provider" import { useRoutes } from "../../../providers/route-provider"
@@ -19,6 +20,7 @@ import UserMenu from "../../molecules/user-menu"
const ICON_SIZE = 20 const ICON_SIZE = 20
const Sidebar: React.FC = () => { const Sidebar: React.FC = () => {
const { t } = useTranslation()
const [currentlyOpen, setCurrentlyOpen] = useState(-1) const [currentlyOpen, setCurrentlyOpen] = useState(-1)
const { isFeatureEnabled } = useFeatureFlag() const { isFeatureEnabled } = useFeatureFlag()
@@ -50,7 +52,9 @@ const Sidebar: React.FC = () => {
</div> </div>
</div> </div>
<div className="my-base flex flex-col px-2"> <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"> <span className="text-grey-90 text-medium font-medium">
{store?.name} {store?.name}
</span> </span>
@@ -60,19 +64,19 @@ const Sidebar: React.FC = () => {
pageLink={"/a/orders"} pageLink={"/a/orders"}
icon={<CartIcon size={ICON_SIZE} />} icon={<CartIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler} triggerHandler={triggerHandler}
text={"Orders"} text={t("sidebar-orders", "Orders")}
/> />
<SidebarMenuItem <SidebarMenuItem
pageLink={"/a/products"} pageLink={"/a/products"}
icon={<TagIcon size={ICON_SIZE} />} icon={<TagIcon size={ICON_SIZE} />}
text={"Products"} text={t("sidebar-products", "Products")}
triggerHandler={triggerHandler} triggerHandler={triggerHandler}
/> />
{isFeatureEnabled("product_categories") && ( {isFeatureEnabled("product_categories") && (
<SidebarMenuItem <SidebarMenuItem
pageLink={"/a/product-categories"} pageLink={"/a/product-categories"}
icon={<SwatchIcon size={ICON_SIZE} />} icon={<SwatchIcon size={ICON_SIZE} />}
text={"Categories"} text={t("sidebar-categories", "Categories")}
triggerHandler={triggerHandler} triggerHandler={triggerHandler}
/> />
)} )}
@@ -80,33 +84,33 @@ const Sidebar: React.FC = () => {
pageLink={"/a/customers"} pageLink={"/a/customers"}
icon={<UsersIcon size={ICON_SIZE} />} icon={<UsersIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler} triggerHandler={triggerHandler}
text={"Customers"} text={t("sidebar-customers", "Customers")}
/> />
{inventoryEnabled && ( {inventoryEnabled && (
<SidebarMenuItem <SidebarMenuItem
pageLink={"/a/inventory"} pageLink={"/a/inventory"}
icon={<BuildingsIcon size={ICON_SIZE} />} icon={<BuildingsIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler} triggerHandler={triggerHandler}
text={"Inventory"} text={t("sidebar-inventory", "Inventory")}
/> />
)} )}
<SidebarMenuItem <SidebarMenuItem
pageLink={"/a/discounts"} pageLink={"/a/discounts"}
icon={<SaleIcon size={ICON_SIZE} />} icon={<SaleIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler} triggerHandler={triggerHandler}
text={"Discounts"} text={t("sidebar-discounts", "Discounts")}
/> />
<SidebarMenuItem <SidebarMenuItem
pageLink={"/a/gift-cards"} pageLink={"/a/gift-cards"}
icon={<GiftIcon size={ICON_SIZE} />} icon={<GiftIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler} triggerHandler={triggerHandler}
text={"Gift Cards"} text={t("sidebar-gift-cards", "Gift Cards")}
/> />
<SidebarMenuItem <SidebarMenuItem
pageLink={"/a/pricing"} pageLink={"/a/pricing"}
icon={<CashIcon size={ICON_SIZE} />} icon={<CashIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler} triggerHandler={triggerHandler}
text={"Pricing"} text={t("sidebar-pricing", "Pricing")}
/> />
{getLinks().map(({ path, label, icon }, index) => { {getLinks().map(({ path, label, icon }, index) => {
const cleanLink = path.replace("/a/", "") const cleanLink = path.replace("/a/", "")
@@ -127,7 +131,7 @@ const Sidebar: React.FC = () => {
pageLink={"/a/settings"} pageLink={"/a/settings"}
icon={<GearIcon size={ICON_SIZE} />} icon={<GearIcon size={ICON_SIZE} />}
triggerHandler={triggerHandler} triggerHandler={triggerHandler}
text={"Settings"} text={t("sidebar-settings", "Settings")}
/> />
</div> </div>
</div> </div>
@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next"
import { SkeletonProvider } from "../../../providers/skeleton-provider" import { SkeletonProvider } from "../../../providers/skeleton-provider"
import Skeleton from "../../atoms/skeleton" import Skeleton from "../../atoms/skeleton"
import ArrowLeftIcon from "../../fundamentals/icons/arrow-left-icon" import ArrowLeftIcon from "../../fundamentals/icons/arrow-left-icon"
@@ -24,6 +25,7 @@ export const TablePagination = ({
hasPrev, hasPrev,
}, },
}: Props) => { }: Props) => {
const { t } = useTranslation()
const soothedOffset = count > 0 ? offset + 1 : 0 const soothedOffset = count > 0 ? offset + 1 : 0
const soothedPageCount = Math.max(1, pageCount) const soothedPageCount = Math.max(1, pageCount)
@@ -35,11 +37,31 @@ export const TablePagination = ({
} }
> >
<Skeleton> <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> </Skeleton>
<div className="flex space-x-4"> <div className="flex space-x-4">
<Skeleton> <Skeleton>
<div>{`${currentPage} of ${soothedPageCount}`}</div> <div>
{t(
"table-container-current-page",
"{{currentPage}} of {{soothedPageCount}}",
{
currentPage,
soothedPageCount,
}
)}
</div>
</Skeleton> </Skeleton>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<button <button
@@ -1,6 +1,7 @@
import clsx from "clsx" import clsx from "clsx"
import { useAdminCreateNote, useAdminOrder } from "medusa-react" import { useAdminCreateNote, useAdminOrder } from "medusa-react"
import React, { useState } from "react" import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import RegisterClaimMenu from "../../../domain/orders/details/claim/register-claim-menu" import RegisterClaimMenu from "../../../domain/orders/details/claim/register-claim-menu"
import ReturnMenu from "../../../domain/orders/details/returns" import ReturnMenu from "../../../domain/orders/details/returns"
@@ -55,6 +56,7 @@ type TimelineProps = {
} }
const Timeline: React.FC<TimelineProps> = ({ orderId }) => { const Timeline: React.FC<TimelineProps> = ({ orderId }) => {
const { t } = useTranslation()
const { orderRelations } = useOrdersExpandParam() const { orderRelations } = useOrdersExpandParam()
const { events, refetch } = useBuildTimeline(orderId) const { events, refetch } = useBuildTimeline(orderId)
@@ -76,17 +78,17 @@ const Timeline: React.FC<TimelineProps> = ({ orderId }) => {
const actions: ActionType[] = [ const actions: ActionType[] = [
{ {
icon: <BackIcon size={20} />, icon: <BackIcon size={20} />,
label: "Request Return", label: t("timeline-request-return", "Request Return"),
onClick: () => setShowRequestReturn(true), onClick: () => setShowRequestReturn(true),
}, },
{ {
icon: <RefreshIcon size={20} />, icon: <RefreshIcon size={20} />,
label: "Register Exchange", label: t("timeline-register-exchange", "Register Exchange"),
onClick: () => setshowCreateSwap(true), onClick: () => setshowCreateSwap(true),
}, },
{ {
icon: <AlertIcon size={20} />, icon: <AlertIcon size={20} />,
label: "Register Claim", label: t("timeline-register-claim", "Register Claim"),
onClick: openRegisterClaim, onClick: openRegisterClaim,
}, },
] ]
@@ -102,8 +104,18 @@ const Timeline: React.FC<TimelineProps> = ({ orderId }) => {
value: value, value: value,
}, },
{ {
onSuccess: () => notification("Success", "Added note", "success"), onSuccess: () =>
onError: (err) => notification("Error", getErrorMessage(err), "error"), 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="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="border-grey-20 py-large px-xlarge border-b">
<div className="flex items-center justify-between"> <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 <div
className={clsx({ className={clsx({
"pointer-events-none opacity-50": !events, "pointer-events-none opacity-50": !events,
@@ -7,6 +7,7 @@ import NotificationBell from "../../molecules/notification-bell"
import SearchBar from "../../molecules/search-bar" import SearchBar from "../../molecules/search-bar"
import ActivityDrawer from "../activity-drawer" import ActivityDrawer from "../activity-drawer"
import MailDialog from "../help-dialog" import MailDialog from "../help-dialog"
import LanguageMenu from "../../molecules/language-menu"
const Topbar: React.FC = () => { const Topbar: React.FC = () => {
const { const {
@@ -1,6 +1,7 @@
import clsx from "clsx" import clsx from "clsx"
import { ReactNode, useState } from "react" import { ReactNode, useState } from "react"
import { useHref } from "react-router-dom" import { useHref } from "react-router-dom"
import { useTranslation } from "react-i18next"
import Tooltip from "../../atoms/tooltip" import Tooltip from "../../atoms/tooltip"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
@@ -83,16 +84,18 @@ type UploadSummaryProps = {
*/ */
function UploadSummary(props: UploadSummaryProps) { function UploadSummary(props: UploadSummaryProps) {
const { creations, updates, type } = props const { creations, updates, type } = props
const { t } = useTranslation()
return ( return (
<div className="flex gap-6"> <div className="flex gap-6">
<div className="text-small text-grey-90 flex items-center"> <div className="text-small text-grey-90 flex items-center">
<CheckCircleIcon color="#9CA3AF" className="mr-2" /> <CheckCircleIcon color="#9CA3AF" className="mr-2" />
<span className="font-semibold"> {creations || 0}&nbsp;</span> new{" "} <span className="font-semibold"> {creations || 0}&nbsp;</span>{" "}
{type} {t("upload-modal-new", "new")} {type}
</div> </div>
<div className="text-small text-grey-90 flex items-center"> <div className="text-small text-grey-90 flex items-center">
<WarningCircleIcon fill="#9CA3AF" className="mr-2" /> <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>
</div> </div>
) )
@@ -106,6 +109,7 @@ type DropAreaProps = {
* Component handles an CSV file drop. * Component handles an CSV file drop.
*/ */
function DropArea(props: DropAreaProps) { function DropArea(props: DropAreaProps) {
const { t } = useTranslation()
const [isDragOver, setIsDragOver] = useState(false) const [isDragOver, setIsDragOver] = useState(false)
const handleFileDrop = (e) => { const handleFileDrop = (e) => {
@@ -139,11 +143,11 @@ function DropArea(props: DropAreaProps) {
)} )}
> >
<span className="text-grey-50 text-small"> <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"> <a className="text-violet-60">
<label className="cursor-pointer" htmlFor="upload-form-file"> <label className="cursor-pointer" htmlFor="upload-form-file">
{" "} {" "}
click to browse. {t("upload-modal-click-to-browse", "click to browse.")}
</label> </label>
<input <input
type="file" type="file"
@@ -156,7 +160,10 @@ function DropArea(props: DropAreaProps) {
</a> </a>
</span> </span>
<span className="text-grey-40 text-small"> <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> </span>
</div> </div>
) )
@@ -203,6 +210,7 @@ function UploadModal(props: UploadModalProps) {
type, type,
} = props } = props
const [uploadFile, setUploadFile] = useState<File>() const [uploadFile, setUploadFile] = useState<File>()
const { t } = useTranslation()
const { name, size } = uploadFile || {} const { name, size } = uploadFile || {}
@@ -222,7 +230,7 @@ function UploadModal(props: UploadModalProps) {
<Modal.Content> <Modal.Content>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-grey-90 inter-large-semibold py-4 text-2xl"> <span className="text-grey-90 inter-large-semibold py-4 text-2xl">
Import {fileTitle} Import {{ fileTitle }}
</span> </span>
<button onClick={onClose} className="text-grey-50 cursor-pointer"> <button onClick={onClose} className="text-grey-50 cursor-pointer">
<CrossIcon size={20} /> <CrossIcon size={20} />
@@ -230,7 +238,9 @@ function UploadModal(props: UploadModalProps) {
</div> </div>
<div className="text-grey-90 inter-large-semibold mb-1 text-base"> <div className="text-grey-90 inter-large-semibold mb-1 text-base">
Import {fileTitle} {t("upload-modal-import-file-title", "Import {{fileTitle}}", {
fileTitle,
})}
</div> </div>
<p className="text-grey-50 mb-4 text-base">{description1Text}</p> <p className="text-grey-50 mb-4 text-base">{description1Text}</p>
@@ -293,7 +303,7 @@ function UploadModal(props: UploadModalProps) {
size="small" size="small"
onClick={onClose} onClick={onClose}
> >
Cancel {t("upload-modal-cancel", "Cancel")}
</Button> </Button>
<Button <Button
@@ -303,7 +313,7 @@ function UploadModal(props: UploadModalProps) {
className="text-small" className="text-small"
onClick={onSubmit} onClick={onSubmit}
> >
Import List {t("upload-modal-import-list", "Import List")}
</Button> </Button>
</div> </div>
</div> </div>
@@ -1,6 +1,7 @@
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import { useAdminProducts } from "medusa-react" import { useAdminProducts } from "medusa-react"
import * as React from "react" import * as React from "react"
import { useTranslation } from "react-i18next"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal" import Modal from "../../molecules/modal"
import { SelectableTable } from "../selectable-table" import { SelectableTable } from "../selectable-table"
@@ -24,6 +25,7 @@ const AddProductsModal = ({
initialSelection, initialSelection,
onSave, onSave,
}: AddProductsModalProps) => { }: AddProductsModalProps) => {
const { t } = useTranslation()
/* ************* Data ************ */ /* ************* Data ************ */
const params = useQueryFilters(defaultQueryProps) const params = useQueryFilters(defaultQueryProps)
@@ -62,7 +64,9 @@ const AddProductsModal = ({
<Modal open handleClose={close}> <Modal open handleClose={close}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={close}> <Modal.Header handleClose={close}>
<h2 className="inter-xlarge-semibold">Add Products</h2> <h2 className="inter-xlarge-semibold">
{t("add-products-modal-add-products", "Add Products")}
</h2>
</Modal.Header> </Modal.Header>
<Modal.Content> <Modal.Content>
<div className="flex h-full min-h-[300px] w-full flex-col justify-between "> <div className="flex h-full min-h-[300px] w-full flex-col justify-between ">
@@ -78,7 +82,10 @@ const AddProductsModal = ({
totalCount={count} totalCount={count}
options={{ options={{
enableSearch: true, enableSearch: true,
searchPlaceholder: "Search by name or description...", searchPlaceholder: t(
"add-products-modal-search-by-name-or-description",
"Search by name or description..."
),
}} }}
{...params} {...params}
/> />
@@ -91,14 +98,14 @@ const AddProductsModal = ({
className="rounded-rounded h-8 w-[128px]" className="rounded-rounded h-8 w-[128px]"
onClick={close} onClick={close}
> >
Cancel {t("add-products-modal-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
className="rounded-rounded h-8 w-[128px]" className="rounded-rounded h-8 w-[128px]"
onClick={handleSave} onClick={handleSave}
> >
Save {t("add-products-modal-save", "Save")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,13 +1,20 @@
import { Product } from "@medusajs/medusa" import { Product } from "@medusajs/medusa"
import clsx from "clsx" import clsx from "clsx"
import { Column, HeaderGroup, Row } from "react-table" import { Column, HeaderGroup, Row } from "react-table"
import { Translation } from "react-i18next"
import ImagePlaceholder from "../../fundamentals/image-placeholder" import ImagePlaceholder from "../../fundamentals/image-placeholder"
import Table from "../../molecules/table" import Table from "../../molecules/table"
import { decideStatus } from "../collection-product-table/utils" import { decideStatus } from "../collection-product-table/utils"
export const columns: Column<Product>[] = [ export const columns: Column<Product>[] = [
{ {
Header: <div className="pl-4">Product Details</div>, Header: (
<div className="pl-4">
<Translation>
{(t) => t("add-products-modal-product-details", "Product Details")}
</Translation>
</div>
),
accessor: "title", accessor: "title",
Cell: ({ row: { original } }) => ( Cell: ({ row: { original } }) => (
<div className="flex w-[400px] items-center pl-4"> <div className="flex w-[400px] items-center pl-4">
@@ -33,7 +40,13 @@ export const columns: Column<Product>[] = [
), ),
}, },
{ {
Header: <div>Status</div>, Header: (
<div>
<Translation>
{(t) => t("add-products-modal-status", "Status")}
</Translation>
</div>
),
accessor: "status", accessor: "status",
Cell: ({ cell: { value } }) => ( Cell: ({ cell: { value } }) => (
<Table.Cell className="pr-base w-[10%]"> <Table.Cell className="pr-base w-[10%]">
@@ -42,7 +55,13 @@ export const columns: Column<Product>[] = [
), ),
}, },
{ {
Header: <div className="flex items-center justify-end pr-4">Variants</div>, Header: (
<div className="flex items-center justify-end pr-4">
<Translation>
{(t) => t("add-products-modal-variants", "Variants")}
</Translation>
</div>
),
accessor: "variants", accessor: "variants",
Cell: ({ row: { original } }) => ( Cell: ({ row: { original } }) => (
<Table.Cell className="flex items-center justify-end pr-4"> <Table.Cell className="flex items-center justify-end pr-4">
@@ -1,4 +1,5 @@
import { Controller } from "react-hook-form" import { Controller } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { Option } from "../../types/shared" import { Option } from "../../types/shared"
import FormValidator from "../../utils/form-validator" import FormValidator from "../../utils/form-validator"
import { nestedForm, NestedForm } from "../../utils/nested-form" import { nestedForm, NestedForm } from "../../utils/nested-form"
@@ -47,11 +48,14 @@ const AddressForm = ({
control, control,
formState: { errors }, formState: { errors },
} = form } = form
const { t } = useTranslation()
return ( return (
<div> <div>
{(type === AddressType.SHIPPING || type === AddressType.BILLING) && ( {(type === AddressType.SHIPPING || type === AddressType.BILLING) && (
<> <>
<span className="inter-base-semibold">General</span> <span className="inter-base-semibold">
{t("templates-general", "General")}
</span>
<div className="gap-large mt-4 mb-8 grid grid-cols-2"> <div className="gap-large mt-4 mb-8 grid grid-cols-2">
<Input <Input
{...register(path("first_name"), { {...register(path("first_name"), {
@@ -60,8 +64,8 @@ const AddressForm = ({
: false, : false,
pattern: FormValidator.whiteSpaceRule("First name"), pattern: FormValidator.whiteSpaceRule("First name"),
})} })}
placeholder="First Name" placeholder={t("templates-first-name", "First Name")}
label="First Name" label={t("templates-first-name", "First Name")}
required={required} required={required}
errors={errors} errors={errors}
/> />
@@ -72,8 +76,8 @@ const AddressForm = ({
: false, : false,
pattern: FormValidator.whiteSpaceRule("Last name"), pattern: FormValidator.whiteSpaceRule("Last name"),
})} })}
placeholder="Last Name" placeholder={t("templates-last-name", "Last Name")}
label="Last Name" label={t("templates-last-name", "Last Name")}
required={required} required={required}
errors={errors} errors={errors}
/> />
@@ -81,14 +85,14 @@ const AddressForm = ({
{...form.register(path("company"), { {...form.register(path("company"), {
pattern: FormValidator.whiteSpaceRule("Company"), pattern: FormValidator.whiteSpaceRule("Company"),
})} })}
placeholder="Company" placeholder={t("templates-company", "Company")}
label="Company" label={t("templates-company", "Company")}
errors={errors} errors={errors}
/> />
<Input <Input
{...form.register(path("phone"))} {...form.register(path("phone"))}
placeholder="Phone" placeholder={t("templates-phone", "Phone")}
label="Phone" label={t("templates-phone", "Phone")}
errors={errors} errors={errors}
/> />
</div> </div>
@@ -98,10 +102,10 @@ const AddressForm = ({
<span className="inter-base-semibold"> <span className="inter-base-semibold">
{`${ {`${
type === AddressType.BILLING type === AddressType.BILLING
? "Billing Address" ? t("templates-billing-address", "Billing Address")
: type === AddressType.SHIPPING : type === AddressType.SHIPPING
? "Shipping Address" ? t("templates-shipping-address", "Shipping Address")
: "Address" : t("templates-address", "Address")
}`} }`}
</span> </span>
)} )}
@@ -111,8 +115,8 @@ const AddressForm = ({
required: required ? FormValidator.required("Address 1") : false, required: required ? FormValidator.required("Address 1") : false,
pattern: FormValidator.whiteSpaceRule("Address 1"), pattern: FormValidator.whiteSpaceRule("Address 1"),
})} })}
placeholder="Address 1" placeholder={t("templates-address-1", "Address 1")}
label="Address 1" label={t("templates-address-1", "Address 1")}
required={required} required={required}
errors={errors} errors={errors}
/> />
@@ -120,8 +124,8 @@ const AddressForm = ({
{...form.register(path("address_2"), { {...form.register(path("address_2"), {
pattern: FormValidator.whiteSpaceRule("Address 2"), pattern: FormValidator.whiteSpaceRule("Address 2"),
})} })}
placeholder="Address 2" placeholder={t("templates-address-2", "Address 2")}
label="Address 2" label={t("templates-address-2", "Address 2")}
errors={errors} errors={errors}
/> />
<Input <Input
@@ -129,15 +133,15 @@ const AddressForm = ({
required: required ? FormValidator.required("Postal code") : false, required: required ? FormValidator.required("Postal code") : false,
pattern: FormValidator.whiteSpaceRule("Postal code"), pattern: FormValidator.whiteSpaceRule("Postal code"),
})} })}
placeholder="Postal code" placeholder={t("templates-postal-code", "Postal code")}
label="Postal code" label={t("templates-postal-code", "Postal code")}
required={required} required={required}
autoComplete="off" autoComplete="off"
errors={errors} errors={errors}
/> />
<Input <Input
placeholder="City" placeholder={t("templates-city", "City")}
label="City" label={t("templates-city", "City")}
{...form.register(path("city"), { {...form.register(path("city"), {
required: required ? FormValidator.required("City") : false, required: required ? FormValidator.required("City") : false,
pattern: FormValidator.whiteSpaceRule("City"), pattern: FormValidator.whiteSpaceRule("City"),
@@ -149,8 +153,8 @@ const AddressForm = ({
{...form.register(path("province"), { {...form.register(path("province"), {
pattern: FormValidator.whiteSpaceRule("Province"), pattern: FormValidator.whiteSpaceRule("Province"),
})} })}
placeholder="Province" placeholder={t("templates-province", "Province")}
label="Province" label={t("templates-province", "Province")}
errors={errors} errors={errors}
/> />
<Controller <Controller
@@ -162,7 +166,7 @@ const AddressForm = ({
render={({ field: { value, onChange } }) => { render={({ field: { value, onChange } }) => {
return ( return (
<NextSelect <NextSelect
label="Country" label={t("templates-country", "Country")}
required={required} required={required}
value={value} value={value}
options={countryOptions} options={countryOptions}
@@ -176,7 +180,9 @@ const AddressForm = ({
/> />
</div> </div>
<div className="mt-xlarge gap-y-base flex flex-col"> <div className="mt-xlarge gap-y-base flex flex-col">
<span className="inter-base-semibold">Metadata</span> <span className="inter-base-semibold">
{t("templates-metadata", "Metadata")}
</span>
<MetadataForm form={nestedForm(form, "metadata")} /> <MetadataForm form={nestedForm(form, "metadata")} />
</div> </div>
</div> </div>
@@ -5,6 +5,7 @@ import {
} from "medusa-react" } from "medusa-react"
import React, { useEffect } from "react" import React, { useEffect } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
import { nestedForm } from "../../../utils/nested-form" import { nestedForm } from "../../../utils/nested-form"
@@ -36,6 +37,7 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
isEdit = false, isEdit = false,
collection, collection,
}) => { }) => {
const { t } = useTranslation()
const { mutate: update, isLoading: updating } = useAdminUpdateCollection( const { mutate: update, isLoading: updating } = useAdminUpdateCollection(
collection?.id! collection?.id!
) )
@@ -93,14 +95,21 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
{ {
onSuccess: () => { onSuccess: () => {
notification( notification(
"Success", t("collection-modal-success", "Success"),
"Successfully updated collection", t(
"collection-modal-successfully-updated-collection",
"Successfully updated collection"
),
"success" "success"
) )
onClose() onClose()
}, },
onError: (error) => { onError: (error) => {
notification("Error", getErrorMessage(error), "error") notification(
t("collection-modal-error", "Error"),
getErrorMessage(error),
"error"
)
}, },
} }
) )
@@ -114,14 +123,21 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
{ {
onSuccess: () => { onSuccess: () => {
notification( notification(
"Success", t("collection-modal-success", "Success"),
"Successfully created collection", t(
"collection-modal-successfully-created-collection",
"Successfully created collection"
),
"success" "success"
) )
onClose() onClose()
}, },
onError: (error) => { onError: (error) => {
notification("Error", getErrorMessage(error), "error") notification(
t("collection-modal-error", "Error"),
getErrorMessage(error),
"error"
)
}, },
} }
) )
@@ -134,37 +150,57 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
<Modal.Header handleClose={onClose}> <Modal.Header handleClose={onClose}>
<div> <div>
<h1 className="inter-xlarge-semibold mb-2xsmall"> <h1 className="inter-xlarge-semibold mb-2xsmall">
{isEdit ? "Edit Collection" : "Add Collection"} {isEdit
? t("collection-modal-edit-collection", "Edit Collection")
: t("collection-modal-add-collection", "Add Collection")}
</h1> </h1>
<p className="inter-small-regular text-grey-50"> <p className="inter-small-regular text-grey-50">
To create a collection, all you need is a title and a handle. {t(
"collection-modal-description",
"To create a collection, all you need is a title and a handle."
)}
</p> </p>
</div> </div>
</Modal.Header> </Modal.Header>
<form onSubmit={handleSubmit(submit)}> <form onSubmit={handleSubmit(submit)}>
<Modal.Content> <Modal.Content>
<div> <div>
<h2 className="inter-base-semibold mb-base">Details</h2> <h2 className="inter-base-semibold mb-base">
{t("collection-modal-details", "Details")}
</h2>
<div className="gap-x-base flex items-center"> <div className="gap-x-base flex items-center">
<InputField <InputField
label="Title" label={t("collection-modal-title-label", "Title")}
required required
placeholder="Sunglasses" placeholder={t(
"collection-modal-title-placeholder",
"Sunglasses"
)}
{...register("title", { required: true })} {...register("title", { required: true })}
/> />
<InputField <InputField
label="Handle" label={t("collection-modal-handle-label", "Handle")}
placeholder="sunglasses" placeholder={t(
"collection-modal-handle-placeholder",
"sunglasses"
)}
{...register("handle")} {...register("handle")}
prefix="/" prefix="/"
tooltip={ tooltip={
<IconTooltip content="URL Slug for the collection. Will be auto generated if left blank." /> <IconTooltip
content={t(
"collection-modal-slug-description",
"URL Slug for the collection. Will be auto generated if left blank."
)}
/>
} }
/> />
</div> </div>
</div> </div>
<div className="mt-xlarge"> <div className="mt-xlarge">
<h2 className="inter-base-semibold mb-base">Metadata</h2> <h2 className="inter-base-semibold mb-base">
{t("collection-modal-metadata", "Metadata")}
</h2>
<MetadataForm form={nestedForm(form, "metadata")} /> <MetadataForm form={nestedForm(form, "metadata")} />
</div> </div>
</Modal.Content> </Modal.Content>
@@ -176,14 +212,19 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
type="button" type="button"
onClick={onClose} onClick={onClose}
> >
Cancel {t("collection-modal-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
size="small" size="small"
loading={isEdit ? updating : creating} loading={isEdit ? updating : creating}
> >
{`${isEdit ? "Save" : "Publish"} collection`} {isEdit
? t("collection-modal-save-collection", "Save collection")
: t(
"collection-modal-publish-collection",
"Publish collection"
)}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,6 +1,7 @@
import { useAdminProducts } from "medusa-react" import { useAdminProducts } from "medusa-react"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { usePagination, useRowSelect, useTable } from "react-table" import { usePagination, useRowSelect, useTable } from "react-table"
import { useTranslation } from "react-i18next"
import { useDebounce } from "../../../hooks/use-debounce" import { useDebounce } from "../../../hooks/use-debounce"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import IndeterminateCheckbox from "../../molecules/indeterminate-checkbox" import IndeterminateCheckbox from "../../molecules/indeterminate-checkbox"
@@ -21,6 +22,7 @@ const AddProductsTable: React.FC<AddProductsTableProps> = ({
onClose, onClose,
}) => { }) => {
const PAGE_SIZE = 10 const PAGE_SIZE = 10
const { t } = useTranslation()
const [query, setQuery] = useState("") const [query, setQuery] = useState("")
const [offset, setOffset] = useState(0) const [offset, setOffset] = useState(0)
const [numPages, setNumPages] = useState(0) const [numPages, setNumPages] = useState(0)
@@ -157,7 +159,9 @@ const AddProductsTable: React.FC<AddProductsTableProps> = ({
<Modal handleClose={onClose}> <Modal handleClose={onClose}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={onClose}> <Modal.Header handleClose={onClose}>
<h3 className="inter-xlarge-semibold">Add Products</h3> <h3 className="inter-xlarge-semibold">
{t("collection-product-table-add-products", "Add Products")}
</h3>
</Modal.Header> </Modal.Header>
<Modal.Content> <Modal.Content>
<TableContainer <TableContainer
@@ -168,7 +172,7 @@ const AddProductsTable: React.FC<AddProductsTableProps> = ({
count: count!, count: count!,
offset: offset, offset: offset,
pageSize: offset + rows.length, pageSize: offset + rows.length,
title: "Products", title: t("collection-product-table-products", "Products"),
currentPage: pageIndex + 1, currentPage: pageIndex + 1,
pageCount: pageCount, pageCount: pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -181,7 +185,10 @@ const AddProductsTable: React.FC<AddProductsTableProps> = ({
enableSearch enableSearch
handleSearch={handleSearch} handleSearch={handleSearch}
searchValue={query} searchValue={query}
searchPlaceholder="Search Products" searchPlaceholder={t(
"collection-product-table-search-products",
"Search Products"
)}
{...getTableProps()} {...getTableProps()}
className="flex-grow" className="flex-grow"
> >
@@ -212,7 +219,7 @@ const AddProductsTable: React.FC<AddProductsTableProps> = ({
className="w-eventButton" className="w-eventButton"
onClick={onClose} onClick={onClose}
> >
Cancel {t("collection-product-table-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
@@ -221,7 +228,7 @@ const AddProductsTable: React.FC<AddProductsTableProps> = ({
onClick={handleSubmit} onClick={handleSubmit}
disabled={disabled} disabled={disabled}
> >
Save {t("collection-product-table-save", "Save")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,6 +1,7 @@
import { useAdminProducts } from "medusa-react" import { useAdminProducts } from "medusa-react"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { Column, usePagination, useRowSelect, useTable } from "react-table" import { Column, usePagination, useRowSelect, useTable } from "react-table"
import { useTranslation } from "react-i18next"
import { useDebounce } from "../../../hooks/use-debounce" import { useDebounce } from "../../../hooks/use-debounce"
import IndeterminateCheckbox from "../../molecules/indeterminate-checkbox" import IndeterminateCheckbox from "../../molecules/indeterminate-checkbox"
import Table from "../../molecules/table" import Table from "../../molecules/table"
@@ -17,6 +18,7 @@ const CollectionProductTable: React.FC<CollectionProductTableProps> = ({
addedProducts, addedProducts,
setProducts, setProducts,
}) => { }) => {
const { t } = useTranslation()
const [query, setQuery] = useState("") const [query, setQuery] = useState("")
const [limit, setLimit] = useState(10) const [limit, setLimit] = useState(10)
const [offset, setOffset] = useState(0) const [offset, setOffset] = useState(0)
@@ -39,18 +41,18 @@ const CollectionProductTable: React.FC<CollectionProductTableProps> = ({
useEffect(() => { useEffect(() => {
setFilteringOptions([ setFilteringOptions([
{ {
title: "Sort by", title: t("collection-product-table-sort-by", "Sort by"),
options: [ options: [
{ {
title: "All", title: t("collection-product-table-all", "All"),
onClick: () => {}, onClick: () => {},
}, },
{ {
title: "Newest", title: t("collection-product-table-newest", "Newest"),
onClick: () => {}, onClick: () => {},
}, },
{ {
title: "Oldest", title: t("collection-product-table-oldest", "Oldest"),
onClick: () => {}, onClick: () => {},
}, },
], ],
@@ -152,7 +154,7 @@ const CollectionProductTable: React.FC<CollectionProductTableProps> = ({
count: count!, count: count!,
offset: offset, offset: offset,
pageSize: offset + rows.length, pageSize: offset + rows.length,
title: "Products", title: t("collection-product-table-products", "Products"),
currentPage: pageIndex + 1, currentPage: pageIndex + 1,
pageCount: pageCount, pageCount: pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -164,7 +166,10 @@ const CollectionProductTable: React.FC<CollectionProductTableProps> = ({
<Table <Table
enableSearch enableSearch
handleSearch={handleSearch} handleSearch={handleSearch}
searchPlaceholder="Search Products" searchPlaceholder={t(
"collection-product-table-search-products",
"Search Products"
)}
filteringOptions={filteringOptions} filteringOptions={filteringOptions}
{...getTableProps()} {...getTableProps()}
className="h-full" className="h-full"
@@ -1,10 +1,12 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { FilteringOptionProps } from "../../molecules/table/filtering-option" import { FilteringOptionProps } from "../../molecules/table/filtering-option"
import { SimpleProductType } from "./utils" import { SimpleProductType } from "./utils"
// TODO: Redo this with server side sorting // TODO: Redo this with server side sorting
const useSortingOptions = (products: SimpleProductType[]) => { const useSortingOptions = (products: SimpleProductType[]) => {
const { t } = useTranslation()
const [options, setOptions] = useState<FilteringOptionProps[]>([]) const [options, setOptions] = useState<FilteringOptionProps[]>([])
const [sortedProducts, setSortedProducts] = const [sortedProducts, setSortedProducts] =
useState<SimpleProductType[]>(products) useState<SimpleProductType[]>(products)
@@ -42,16 +44,16 @@ const useSortingOptions = (products: SimpleProductType[]) => {
useEffect(() => { useEffect(() => {
setOptions([ setOptions([
{ {
title: "Sort by", title: t("collection-product-table-sort-by", "Sort by"),
options: [ options: [
{ {
title: "All", title: t("collection-product-table-all", "All"),
onClick: () => { onClick: () => {
setSortedProducts(products) setSortedProducts(products)
}, },
}, },
{ {
title: "Newest", title: t("collection-product-table-newest", "Newest"),
onClick: () => { onClick: () => {
const sorted = products.sort(sortByNewest) const sorted = products.sort(sortByNewest)
console.log(sorted) console.log(sorted)
@@ -59,7 +61,7 @@ const useSortingOptions = (products: SimpleProductType[]) => {
}, },
}, },
{ {
title: "Oldest", title: t("collection-product-table-oldest", "Oldest"),
onClick: () => { onClick: () => {
const sorted = products.sort(sortByOldest) const sorted = products.sort(sortByOldest)
console.log(sorted) console.log(sorted)
@@ -67,7 +69,7 @@ const useSortingOptions = (products: SimpleProductType[]) => {
}, },
}, },
{ {
title: "Title", title: t("collection-product-table-title", "Title"),
onClick: () => { onClick: () => {
const sorted = products.sort(sortByTitle) const sorted = products.sort(sortByTitle)
console.log(sorted) console.log(sorted)
@@ -1,3 +1,4 @@
import { Translation } from "react-i18next"
import StatusIndicator from "../../fundamentals/status-indicator" import StatusIndicator from "../../fundamentals/status-indicator"
export type SimpleProductType = { export type SimpleProductType = {
@@ -11,13 +12,52 @@ export type SimpleProductType = {
export const decideStatus = (status: string) => { export const decideStatus = (status: string) => {
switch (status) { switch (status) {
case "published": case "published":
return <StatusIndicator title="Published" variant="success" /> return (
<Translation>
{(t) => (
<StatusIndicator
title={t(
"collection-product-table-decide-status-published",
"Published"
)}
variant="success"
/>
)}
</Translation>
)
case "draft": case "draft":
return <StatusIndicator title="Draft" variant="default" /> return (
<Translation>
{(t) => (
<StatusIndicator
title={t("collection-product-table-draft", "Draft")}
variant="default"
/>
)}
</Translation>
)
case "proposed": case "proposed":
return <StatusIndicator title="Proposed" variant="warning" /> return (
<Translation>
{(t) => (
<StatusIndicator
title={t("collection-product-table-proposed", "Proposed")}
variant="warning"
/>
)}
</Translation>
)
case "rejected": case "rejected":
return <StatusIndicator title="Rejected" variant="danger" /> return (
<Translation>
{(t) => (
<StatusIndicator
title={t("collection-product-table-rejected", "Rejected")}
variant="danger"
/>
)}
</Translation>
)
default: default:
return null return null
} }
@@ -1,6 +1,7 @@
import { useAdminProducts } from "medusa-react" import { useAdminProducts } from "medusa-react"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { usePagination, useTable } from "react-table" import { usePagination, useTable } from "react-table"
import { useTranslation } from "react-i18next"
import { useDebounce } from "../../../hooks/use-debounce" import { useDebounce } from "../../../hooks/use-debounce"
import Medusa from "../../../services/api" import Medusa from "../../../services/api"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
@@ -25,6 +26,7 @@ const ViewProductsTable: React.FC<ViewProductsTableProps> = ({
const [numPages, setNumPages] = useState(0) const [numPages, setNumPages] = useState(0)
const [currentPage, setCurrentPage] = useState(0) const [currentPage, setCurrentPage] = useState(0)
const debouncedSearchTerm = useDebounce(query, 500) const debouncedSearchTerm = useDebounce(query, 500)
const { t } = useTranslation()
const [showDelete, setShowDelete] = useState(false) const [showDelete, setShowDelete] = useState(false)
const [idToDelete, setIdToDelete] = useState<string | undefined>(undefined) const [idToDelete, setIdToDelete] = useState<string | undefined>(undefined)
@@ -157,7 +159,10 @@ const ViewProductsTable: React.FC<ViewProductsTableProps> = ({
<Table <Table
enableSearch enableSearch
handleSearch={handleSearch} handleSearch={handleSearch}
searchPlaceholder="Search Products" searchPlaceholder={t(
"collection-product-table-search-products",
"Search Products"
)}
{...getTableProps()} {...getTableProps()}
className="h-full" className="h-full"
> >
@@ -183,8 +188,14 @@ const ViewProductsTable: React.FC<ViewProductsTableProps> = ({
<DeletePrompt <DeletePrompt
onDelete={async () => handleRemoveProduct()} onDelete={async () => handleRemoveProduct()}
handleClose={() => setShowDelete(!showDelete)} handleClose={() => setShowDelete(!showDelete)}
heading="Remove product from collection" heading={t(
successText="Product removed from collection" "collection-product-table-remove-product-from-collection",
"Remove product from collection"
)}
successText={t(
"collection-product-table-product-removed-from-collection",
"Product removed from collection"
)}
/> />
)} )}
</> </>
@@ -1,19 +1,24 @@
import { useAdminDeleteCollection } from "medusa-react" import { useAdminDeleteCollection } from "medusa-react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import useImperativeDialog from "../../../hooks/use-imperative-dialog" import useImperativeDialog from "../../../hooks/use-imperative-dialog"
import EditIcon from "../../fundamentals/icons/edit-icon" import EditIcon from "../../fundamentals/icons/edit-icon"
import TrashIcon from "../../fundamentals/icons/trash-icon" import TrashIcon from "../../fundamentals/icons/trash-icon"
import { ActionType } from "../../molecules/actionables" import { ActionType } from "../../molecules/actionables"
const useCollectionActions = (collection) => { const useCollectionActions = (collection) => {
const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const dialog = useImperativeDialog() const dialog = useImperativeDialog()
const deleteCollection = useAdminDeleteCollection(collection?.id) const deleteCollection = useAdminDeleteCollection(collection?.id)
const handleDelete = async () => { const handleDelete = async () => {
const shouldDelete = await dialog({ const shouldDelete = await dialog({
heading: "Delete Collection", heading: t("collections-table-delete-collection", "Delete Collection"),
text: "Are you sure you want to delete this collection?", text: t(
"collections-table-confirm-delete",
"Are you sure you want to delete this collection?"
),
}) })
if (shouldDelete) { if (shouldDelete) {
@@ -23,12 +28,12 @@ const useCollectionActions = (collection) => {
const getActions = (coll): ActionType[] => [ const getActions = (coll): ActionType[] => [
{ {
label: "Edit", label: t("collections-table-edit", "Edit"),
onClick: () => navigate(`/a/collections/${coll.id}`), onClick: () => navigate(`/a/collections/${coll.id}`),
icon: <EditIcon size={20} />, icon: <EditIcon size={20} />,
}, },
{ {
label: "Delete", label: t("collections-table-delete", "Delete"),
variant: "danger", variant: "danger",
onClick: handleDelete, onClick: handleDelete,
icon: <TrashIcon size={20} />, icon: <TrashIcon size={20} />,
@@ -1,24 +1,26 @@
import moment from "moment" import moment from "moment"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import Tooltip from "../../atoms/tooltip" import Tooltip from "../../atoms/tooltip"
const useCollectionTableColumn = () => { const useCollectionTableColumn = () => {
const { t } = useTranslation()
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
Header: "Title", Header: t("collections-table-title", "Title"),
accessor: "title", accessor: "title",
Cell: ({ row: { original } }) => { Cell: ({ row: { original } }) => {
return <div className="flex items-center">{original.title}</div> return <div className="flex items-center">{original.title}</div>
}, },
}, },
{ {
Header: "Handle", Header: t("collections-table-handle", "Handle"),
accessor: "handle", accessor: "handle",
Cell: ({ cell: { value } }) => <div>/{value}</div>, Cell: ({ cell: { value } }) => <div>/{value}</div>,
}, },
{ {
Header: "Created At", Header: t("collections-table-created-at", "Created At"),
accessor: "created_at", accessor: "created_at",
Cell: ({ cell: { value } }) => ( Cell: ({ cell: { value } }) => (
<Tooltip content={moment(value).format("DD MMM YYYY hh:mm A")}> <Tooltip content={moment(value).format("DD MMM YYYY hh:mm A")}>
@@ -27,7 +29,7 @@ const useCollectionTableColumn = () => {
), ),
}, },
{ {
Header: "Updated At", Header: t("collections-table-updated-at", "Updated At"),
accessor: "updated_at", accessor: "updated_at",
Cell: ({ cell: { value } }) => ( Cell: ({ cell: { value } }) => (
<Tooltip content={moment(value).format("DD MMM YYYY hh:mm A")}> <Tooltip content={moment(value).format("DD MMM YYYY hh:mm A")}>
@@ -36,7 +38,7 @@ const useCollectionTableColumn = () => {
), ),
}, },
{ {
Header: "Products", Header: t("collections-table-products", "Products"),
accessor: "products", accessor: "products",
Cell: ({ cell: { value } }) => { Cell: ({ cell: { value } }) => {
return <div>{value?.length || "-"}</div> return <div>{value?.length || "-"}</div>
@@ -13,6 +13,7 @@ import {
useSortBy, useSortBy,
useTable, useTable,
} from "react-table" } from "react-table"
import { useTranslation } from "react-i18next"
import useQueryFilters from "../../../hooks/use-query-filters" import useQueryFilters from "../../../hooks/use-query-filters"
import useSetSearchParams from "../../../hooks/use-set-search-params" import useSetSearchParams from "../../../hooks/use-set-search-params"
import DetailsIcon from "../../fundamentals/details-icon" import DetailsIcon from "../../fundamentals/details-icon"
@@ -95,22 +96,34 @@ function CustomerGroupsTableRow(props: CustomerGroupsTableRowProps) {
const navigate = useNavigate() const navigate = useNavigate()
const notification = useNotification() const notification = useNotification()
const { mutate } = useAdminDeleteCustomerGroup(row.original.id) const { mutate } = useAdminDeleteCustomerGroup(row.original.id)
const { t } = useTranslation()
const actions: ActionType[] = [ const actions: ActionType[] = [
{ {
label: "Details", label: t("customer-group-table-details", "Details"),
onClick: () => navigate(row.original.id), onClick: () => navigate(row.original.id),
icon: <DetailsIcon size={20} />, icon: <DetailsIcon size={20} />,
}, },
{ {
label: "Delete", label: t("customer-group-table-delete", "Delete"),
onClick: () => { onClick: () => {
mutate(undefined, { mutate(undefined, {
onSuccess: () => { onSuccess: () => {
notification("Success", "Group deleted", "success") notification(
t("customer-group-table-success", "Success"),
t("customer-group-table-group-deleted", "Group deleted"),
"success"
)
}, },
onError: () => { onError: () => {
notification("Error", "Failed to delete the group", "error") notification(
t("customer-group-table-error", "Error"),
t(
"customer-group-table-failed-to-delete-the-group",
"Failed to delete the group"
),
"error"
)
}, },
}) })
}, },
@@ -151,6 +164,7 @@ type CustomerGroupsTableProps = ReturnType<typeof useQueryFilters> & {
function CustomerGroupsTable(props: CustomerGroupsTableProps) { function CustomerGroupsTable(props: CustomerGroupsTableProps) {
const { customerGroups, queryObject, count, paginate, setQuery, isLoading } = const { customerGroups, queryObject, count, paginate, setQuery, isLoading } =
props props
const { t } = useTranslation()
const tableConfig: TableOptions<CustomerGroup> = { const tableConfig: TableOptions<CustomerGroup> = {
columns: CUSTOMER_GROUPS_TABLE_COLUMNS, columns: CUSTOMER_GROUPS_TABLE_COLUMNS,
@@ -209,7 +223,7 @@ function CustomerGroupsTable(props: CustomerGroupsTableProps) {
count: count, count: count,
offset: queryObject.offset, offset: queryObject.offset,
pageSize: queryObject.offset + table.rows.length, pageSize: queryObject.offset + table.rows.length,
title: "Customer groups", title: t("customer-group-table-customer-groups", "Customer groups"),
currentPage: table.state.pageIndex + 1, currentPage: table.state.pageIndex + 1,
pageCount: table.pageCount, pageCount: table.pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -8,6 +8,7 @@ import {
} from "react-table" } from "react-table"
import { Customer } from "@medusajs/medusa" import { Customer } from "@medusajs/medusa"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import useQueryFilters from "../../../hooks/use-query-filters" import useQueryFilters from "../../../hooks/use-query-filters"
@@ -81,10 +82,11 @@ function CustomersListTableRow(props: CustomersListTableRowProps) {
const { row, removeCustomers } = props const { row, removeCustomers } = props
const navigate = useNavigate() const navigate = useNavigate()
const { t } = useTranslation()
const actions = [ const actions = [
{ {
label: "Details", label: t("customer-group-table-details", "Details"),
onClick: () => navigate(`/a/customers/${row.original.id}`), onClick: () => navigate(`/a/customers/${row.original.id}`),
icon: <DetailsIcon size={20} />, icon: <DetailsIcon size={20} />,
}, },
@@ -94,7 +96,10 @@ function CustomersListTableRow(props: CustomersListTableRowProps) {
// icon: <MailIcon size={20} />, // icon: <MailIcon size={20} />,
// }, // },
{ {
label: "Delete from the group", label: t(
"customer-group-table-delete-from-the-group",
"Delete from the group"
),
variant: "danger", variant: "danger",
onClick: () => onClick: () =>
removeCustomers({ removeCustomers({
@@ -124,6 +129,7 @@ function CustomersListTableRow(props: CustomersListTableRowProps) {
* Render a list of customers that belong to a customer group. * Render a list of customers that belong to a customer group.
*/ */
function CustomersListTable(props: CustomersListTableProps) { function CustomersListTable(props: CustomersListTableProps) {
const { t } = useTranslation()
const { const {
customers, customers,
removeCustomers, removeCustomers,
@@ -186,7 +192,10 @@ function CustomersListTable(props: CustomersListTableProps) {
count: count!, count: count!,
offset: queryObject.offset, offset: queryObject.offset,
pageSize: queryObject.offset + table.rows.length, pageSize: queryObject.offset + table.rows.length,
title: "Customer Groups", title: t(
"customer-group-table-customer-groups-title",
"Customer Groups"
),
currentPage: table.state.pageIndex + 1, currentPage: table.state.pageIndex + 1,
pageCount: table.pageCount, pageCount: table.pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -1,6 +1,7 @@
import { Customer } from "@medusajs/medusa" import { Customer } from "@medusajs/medusa"
import { useAdminCustomerGroups, useAdminCustomers } from "medusa-react" import { useAdminCustomerGroups, useAdminCustomers } from "medusa-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { import {
HeaderGroup, HeaderGroup,
Row, Row,
@@ -74,6 +75,7 @@ type EditCustomersTableProps = {
* Container for the "edit customers" table. * Container for the "edit customers" table.
*/ */
function EditCustomersTable(props: EditCustomersTableProps) { function EditCustomersTable(props: EditCustomersTableProps) {
const { t } = useTranslation()
const { setSelectedCustomerIds, selectedCustomerIds, handleSubmit, onClose } = const { setSelectedCustomerIds, selectedCustomerIds, handleSubmit, onClose } =
props props
@@ -131,10 +133,10 @@ function EditCustomersTable(props: EditCustomersTableProps) {
const filteringOptions = [ const filteringOptions = [
{ {
title: "Groups", title: t("customer-group-table-groups", "Groups"),
options: [ options: [
{ {
title: "All", title: t("customer-group-table-all", "All"),
onClick: () => setActiveGroupId(null), onClick: () => setActiveGroupId(null),
}, },
...(customer_groups || []).map((g) => ({ ...(customer_groups || []).map((g) => ({
@@ -176,7 +178,9 @@ function EditCustomersTable(props: EditCustomersTableProps) {
<Modal handleClose={onClose}> <Modal handleClose={onClose}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={onClose}> <Modal.Header handleClose={onClose}>
<h3 className="inter-xlarge-semibold">Edit Customers</h3> <h3 className="inter-xlarge-semibold">
{t("customer-group-table-edit-customers", "Edit Customers")}
</h3>
</Modal.Header> </Modal.Header>
<Modal.Content> <Modal.Content>
@@ -188,7 +192,7 @@ function EditCustomersTable(props: EditCustomersTableProps) {
count: count!, count: count!,
offset: queryObject.offset, offset: queryObject.offset,
pageSize: queryObject.offset + table.rows.length, pageSize: queryObject.offset + table.rows.length,
title: "Customers", title: t("customer-group-table-customers", "Customers"),
currentPage: table.state.pageIndex + 1, currentPage: table.state.pageIndex + 1,
pageCount: table.pageCount, pageCount: table.pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -228,7 +232,7 @@ function EditCustomersTable(props: EditCustomersTableProps) {
className="w-eventButton" className="w-eventButton"
onClick={onClose} onClick={onClose}
> >
Cancel {t("customer-group-table-cancel", "Cancel")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
@@ -236,7 +240,7 @@ function EditCustomersTable(props: EditCustomersTableProps) {
className="w-eventButton" className="w-eventButton"
onClick={handleSubmit} onClick={handleSubmit}
> >
Save {t("customer-group-table-save", "Save")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -2,6 +2,7 @@ import { Order } from "@medusajs/medusa"
import { useAdminOrders } from "medusa-react" import { useAdminOrders } from "medusa-react"
import { useState } from "react" import { useState } from "react"
import { useTable, usePagination } from "react-table" import { useTable, usePagination } from "react-table"
import { useTranslation } from "react-i18next"
import RefreshIcon from "../../fundamentals/icons/refresh-icon" import RefreshIcon from "../../fundamentals/icons/refresh-icon"
import Table from "../../molecules/table" import Table from "../../molecules/table"
import TableContainer from "../../organisms/table-container" import TableContainer from "../../organisms/table-container"
@@ -15,6 +16,7 @@ type Props = {
} }
const CustomerOrdersTable = ({ id }: Props) => { const CustomerOrdersTable = ({ id }: Props) => {
const { t } = useTranslation()
const [selectedOrderForTransfer, setSelectedOrderForTransfer] = const [selectedOrderForTransfer, setSelectedOrderForTransfer] =
useState<Order | null>(null) useState<Order | null>(null)
@@ -85,7 +87,7 @@ const CustomerOrdersTable = ({ id }: Props) => {
count: count!, count: count!,
offset, offset,
pageSize: offset + rows.length, pageSize: offset + rows.length,
title: "Orders", title: t("customer-orders-table-orders", "Orders"),
currentPage: pageIndex + 1, currentPage: pageIndex + 1,
pageCount: pageCount, pageCount: pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -118,7 +120,10 @@ const CustomerOrdersTable = ({ id }: Props) => {
forceDropdown forceDropdown
actions={[ actions={[
{ {
label: "Transfer order", label: t(
"customer-orders-table-transfer-order",
"Transfer order"
),
icon: <RefreshIcon size={"20"} />, icon: <RefreshIcon size={"20"} />,
onClick: () => { onClick: () => {
setSelectedOrderForTransfer(row.original as Order) setSelectedOrderForTransfer(row.original as Order)
@@ -2,49 +2,113 @@ import { Order } from "@medusajs/medusa"
import moment from "moment" import moment from "moment"
import { useMemo, useRef } from "react" import { useMemo, useRef } from "react"
import { Column } from "react-table" import { Column } from "react-table"
import { useTranslation } from "react-i18next"
import { useObserveWidth } from "../../../hooks/use-observe-width" import { useObserveWidth } from "../../../hooks/use-observe-width"
import { stringDisplayPrice } from "../../../utils/prices" import { stringDisplayPrice } from "../../../utils/prices"
import Tooltip from "../../atoms/tooltip" import Tooltip from "../../atoms/tooltip"
import ImagePlaceholder from "../../fundamentals/image-placeholder" import ImagePlaceholder from "../../fundamentals/image-placeholder"
import StatusIndicator from "../../fundamentals/status-indicator" import StatusIndicator from "../../fundamentals/status-indicator"
import { TFunction } from "i18next"
const decidePaymentStatus = (status: string) => { const decidePaymentStatus = (status: string, t: TFunction) => {
switch (status) { switch (status) {
case "captured": case "captured":
return <StatusIndicator variant="success" title={"Paid"} /> return (
<StatusIndicator
variant="success"
title={t("customer-orders-table-paid", "Paid")}
/>
)
case "awaiting": case "awaiting":
return <StatusIndicator variant="warning" title={"Awaiting"} /> return (
<StatusIndicator
variant="warning"
title={t("customer-orders-table-awaiting", "Awaiting")}
/>
)
case "requires": case "requires":
return <StatusIndicator variant="danger" title={"Requires action"} /> return (
<StatusIndicator
variant="danger"
title={t("customer-orders-table-requires-action", "Requires action")}
/>
)
default: default:
return <StatusIndicator variant="primary" title={"N/A"} /> return (
<StatusIndicator
variant="primary"
title={t("customer-orders-table-n-a", "N/A")}
/>
)
} }
} }
const decideFulfillmentStatus = (status: string) => { const decideFulfillmentStatus = (status: string, t: TFunction) => {
switch (status) { switch (status) {
case "fulfilled": case "fulfilled":
return <StatusIndicator variant="success" title={"Fulfilled"} /> return (
<StatusIndicator
variant="success"
title={t("customer-orders-table-fulfilled", "Fulfilled")}
/>
)
case "shipped": case "shipped":
return <StatusIndicator variant="success" title={"Shipped"} /> return (
<StatusIndicator
variant="success"
title={t("customer-orders-table-shipped", "Shipped")}
/>
)
case "not_fulfilled": case "not_fulfilled":
return <StatusIndicator variant="default" title={"Not fulfilled"} /> return (
<StatusIndicator
variant="default"
title={t("customer-orders-table-not-fulfilled", "Not fulfilled")}
/>
)
case "partially_fulfilled": case "partially_fulfilled":
return <StatusIndicator variant="warning" title={"Partially fulfilled"} /> return (
<StatusIndicator
variant="warning"
title={t(
"customer-orders-table-partially-fulfilled",
"Partially fulfilled"
)}
/>
)
case "partially_shipped": case "partially_shipped":
return <StatusIndicator variant="warning" title={"Partially shipped"} /> return (
<StatusIndicator
variant="warning"
title={t(
"customer-orders-table-partially-shipped",
"Partially shipped"
)}
/>
)
case "requires": case "requires":
return <StatusIndicator variant="danger" title={"Requires action"} /> return (
<StatusIndicator
variant="danger"
title={t("customer-orders-table-requires-action", "Requires action")}
/>
)
default: default:
return <StatusIndicator variant="primary" title={"N/A"} /> return (
<StatusIndicator
variant="primary"
title={t("customer-orders-table-n-a", "N/A")}
/>
)
} }
} }
export const useCustomerOrdersColumns = (): Column<Order>[] => { export const useCustomerOrdersColumns = (): Column<Order>[] => {
const { t } = useTranslation()
const columns = useMemo(() => { const columns = useMemo(() => {
return [ return [
{ {
Header: "Order", Header: t("customer-orders-table-order", "Order"),
accessor: "display_id", accessor: "display_id",
Cell: ({ value }) => { Cell: ({ value }) => {
return <span className="text-grey-90">#{value}</span> return <span className="text-grey-90">#{value}</span>
@@ -95,7 +159,11 @@ export const useCustomerOrdersColumns = (): Column<Order>[] => {
</div> </div>
{remainder > 0 && ( {remainder > 0 && (
<span className="text-grey-40 inter-small-regular"> <span className="text-grey-40 inter-small-regular">
+ {remainder} more {t(
"customer-orders-table-remainder-more",
"+ {{remainder}} more",
{ remainder }
)}
</span> </span>
)} )}
</div> </div>
@@ -103,28 +171,32 @@ export const useCustomerOrdersColumns = (): Column<Order>[] => {
}, },
}, },
{ {
Header: "Date", Header: t("customer-orders-table-date", "Date"),
accessor: "created_at", accessor: "created_at",
Cell: ({ value }) => { Cell: ({ value }) => {
return moment(value).format("DD MMM YYYY hh:mm") return moment(value).format("DD MMM YYYY hh:mm")
}, },
}, },
{ {
Header: "Fulfillment", Header: t("customer-orders-table-fulfillment", "Fulfillment"),
accessor: "fulfillment_status", accessor: "fulfillment_status",
Cell: ({ value }) => { Cell: ({ value }) => {
return decideFulfillmentStatus(value) return decideFulfillmentStatus(value, t)
}, },
}, },
{ {
Header: "Status", Header: t("customer-orders-table-status", "Status"),
accessor: "payment_status", accessor: "payment_status",
Cell: ({ value }) => { Cell: ({ value }) => {
return decidePaymentStatus(value) return decidePaymentStatus(value, t)
}, },
}, },
{ {
Header: () => <div className="text-right">Total</div>, Header: () => (
<div className="text-right">
{t("customer-orders-table-total", "Total")}
</div>
),
accessor: "total", accessor: "total",
Cell: ({ Cell: ({
value, value,
@@ -3,6 +3,7 @@ import { useAdminCustomers } from "medusa-react"
import qs from "qs" import qs from "qs"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import { usePagination, useTable } from "react-table" import { usePagination, useTable } from "react-table"
import DetailsIcon from "../../fundamentals/details-icon" import DetailsIcon from "../../fundamentals/details-icon"
import EditIcon from "../../fundamentals/icons/edit-icon" import EditIcon from "../../fundamentals/icons/edit-icon"
@@ -19,6 +20,7 @@ const defaultQueryProps = {
const CustomerTable = () => { const CustomerTable = () => {
const navigate = useNavigate() const navigate = useNavigate()
const { t } = useTranslation()
const { const {
reset, reset,
@@ -139,7 +141,7 @@ const CustomerTable = () => {
count: count!, count: count!,
offset: queryObject.offset, offset: queryObject.offset,
pageSize: queryObject.offset + rows.length, pageSize: queryObject.offset + rows.length,
title: "Customers", title: t("customer-table-customers", "Customers"),
currentPage: pageIndex + 1, currentPage: pageIndex + 1,
pageCount: pageCount, pageCount: pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -174,12 +176,12 @@ const CustomerTable = () => {
color={"inherit"} color={"inherit"}
actions={[ actions={[
{ {
label: "Edit", label: t("customer-table-edit", "Edit"),
onClick: () => navigate(row.original.id), onClick: () => navigate(row.original.id),
icon: <EditIcon size={20} />, icon: <EditIcon size={20} />,
}, },
{ {
label: "Details", label: t("customer-table-details", "Details"),
onClick: () => navigate(row.original.id), onClick: () => navigate(row.original.id),
icon: <DetailsIcon size={20} />, icon: <DetailsIcon size={20} />,
}, },
@@ -1,18 +1,20 @@
import moment from "moment" import moment from "moment"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { getColor } from "../../../utils/color" import { getColor } from "../../../utils/color"
import CustomerAvatarItem from "../../molecules/customer-avatar-item" import CustomerAvatarItem from "../../molecules/customer-avatar-item"
export const useCustomerColumns = () => { export const useCustomerColumns = () => {
const { t } = useTranslation()
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
Header: "Date added", Header: t("customer-table-date-added", "Date added"),
accessor: "created_at", // accessor is the "key" in the data accessor: "created_at", // accessor is the "key" in the data
Cell: ({ cell: { value } }) => moment(value).format("DD MMM YYYY"), Cell: ({ cell: { value } }) => moment(value).format("DD MMM YYYY"),
}, },
{ {
Header: "Name", Header: t("customer-table-name", "Name"),
accessor: "customer", accessor: "customer",
Cell: ({ row }) => ( Cell: ({ row }) => (
<CustomerAvatarItem <CustomerAvatarItem
@@ -22,7 +24,7 @@ export const useCustomerColumns = () => {
), ),
}, },
{ {
Header: "Email", Header: t("customer-table-email", "Email"),
accessor: "email", accessor: "email",
}, },
{ {
@@ -31,7 +33,11 @@ export const useCustomerColumns = () => {
}, },
{ {
accessor: "orders", accessor: "orders",
Header: () => <div className="text-right">Orders</div>, Header: () => (
<div className="text-right">
{t("customer-table-orders", "Orders")}
</div>
),
Cell: ({ cell: { value } }) => ( Cell: ({ cell: { value } }) => (
<div className="text-right">{value?.length || 0}</div> <div className="text-right">{value?.length || 0}</div>
), ),
@@ -1,5 +1,6 @@
import clsx from "clsx" import clsx from "clsx"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import FilterDropdownContainer from "../../../components/molecules/filter-dropdown/container" import FilterDropdownContainer from "../../../components/molecules/filter-dropdown/container"
import FilterDropdownItem from "../../../components/molecules/filter-dropdown/item" import FilterDropdownItem from "../../../components/molecules/filter-dropdown/item"
import SaveFilterItem from "../../../components/molecules/filter-dropdown/save-field" import SaveFilterItem from "../../../components/molecules/filter-dropdown/save-field"
@@ -26,6 +27,7 @@ const DiscountFilters = ({
submitFilters, submitFilters,
clearFilters, clearFilters,
}) => { }) => {
const { t } = useTranslation()
const [tempState, setTempState] = useState(filters) const [tempState, setTempState] = useState(filters)
const [name, setName] = useState("") const [name, setName] = useState("")
@@ -88,7 +90,7 @@ const DiscountFilters = ({
)} )}
> >
<div className="rounded-rounded bg-grey-5 border-grey-20 inter-small-semibold flex h-6 items-center border px-2"> <div className="rounded-rounded bg-grey-5 border-grey-20 inter-small-semibold flex h-6 items-center border px-2">
Filters {t("discount-filter-dropdown-filters", "Filters")}
<div className="text-grey-40 ml-1 flex items-center rounded"> <div className="text-grey-40 ml-1 flex items-center rounded">
<span className="text-violet-60 inter-small-semibold"> <span className="text-violet-60 inter-small-semibold">
{numberOfFilters ? numberOfFilters : "0"} {numberOfFilters ? numberOfFilters : "0"}
@@ -3,6 +3,7 @@ import { useAdminDiscounts } from "medusa-react"
import qs from "qs" import qs from "qs"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { usePagination, useTable } from "react-table" import { usePagination, useTable } from "react-table"
import { useTranslation } from "react-i18next"
import { useAnalytics } from "../../../providers/analytics-provider" import { useAnalytics } from "../../../providers/analytics-provider"
import Table from "../../molecules/table" import Table from "../../molecules/table"
import TableContainer from "../../organisms/table-container" import TableContainer from "../../organisms/table-container"
@@ -16,6 +17,7 @@ const DEFAULT_PAGE_SIZE = 15
const defaultQueryProps = {} const defaultQueryProps = {}
const DiscountTable: React.FC = () => { const DiscountTable: React.FC = () => {
const { t } = useTranslation()
const { const {
removeTab, removeTab,
setTab, setTab,
@@ -157,7 +159,7 @@ const DiscountTable: React.FC = () => {
count: count!, count: count!,
offset: queryObject.offset, offset: queryObject.offset,
pageSize: queryObject.offset + rows.length, pageSize: queryObject.offset + rows.length,
title: "Discounts", title: t("discount-table-discounts", "Discounts"),
currentPage: pageIndex + 1, currentPage: pageIndex + 1,
pageCount: pageCount, pageCount: pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -181,7 +183,10 @@ const DiscountTable: React.FC = () => {
} }
enableSearch enableSearch
handleSearch={setQuery} handleSearch={setQuery}
searchPlaceholder="Search by code or description..." searchPlaceholder={t(
"discount-table-search-by-code-or-description",
"Search by code or description..."
)}
searchValue={query} searchValue={query}
{...getTableProps()} {...getTableProps()}
> >
@@ -1,10 +1,12 @@
import { useAdminCreateDiscount } from "medusa-react" import { useAdminCreateDiscount } from "medusa-react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
import { removeFalsy } from "../../../utils/remove-nullish" import { removeFalsy } from "../../../utils/remove-nullish"
const useCopyPromotion = () => { const useCopyPromotion = () => {
const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const notification = useNotification() const notification = useNotification()
const createPromotion = useAdminCreateDiscount() const createPromotion = useAdminCreateDiscount()
@@ -60,10 +62,21 @@ const useCopyPromotion = () => {
await createPromotion.mutate(copy, { await createPromotion.mutate(copy, {
onSuccess: (result) => { onSuccess: (result) => {
navigate(`/a/discounts/${result.discount.id}`) navigate(`/a/discounts/${result.discount.id}`)
notification("Success", "Successfully copied discount", "success") notification(
t("discount-table-success", "Success"),
t(
"discount-table-successfully-copied-discount",
"Successfully copied discount"
),
"success"
)
}, },
onError: (err) => { onError: (err) => {
notification("Error", getErrorMessage(err), "error") notification(
t("discount-table-error", "Error"),
getErrorMessage(err),
"error"
)
}, },
}) })
} }
@@ -1,5 +1,6 @@
import { end, parse } from "iso8601-duration" import { end, parse } from "iso8601-duration"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { formatAmountWithSymbol } from "../../../utils/prices" import { formatAmountWithSymbol } from "../../../utils/prices"
import Badge from "../../fundamentals/badge" import Badge from "../../fundamentals/badge"
import StatusDot from "../../fundamentals/status-indicator" import StatusDot from "../../fundamentals/status-indicator"
@@ -34,19 +35,44 @@ const getPromotionStatus = (promotion) => {
return PromotionStatus.DISABLED return PromotionStatus.DISABLED
} }
const getPromotionStatusDot = (promotion) => { const getPromotionStatusDot = (promotion, t) => {
const status = getPromotionStatus(promotion) const status = getPromotionStatus(promotion)
switch (status) { switch (status) {
case PromotionStatus.SCHEDULED: case PromotionStatus.SCHEDULED:
return <StatusDot title="Scheduled" variant="warning" /> return (
<StatusDot
title={t("discount-table-scheduled", "Scheduled")}
variant="warning"
/>
)
case PromotionStatus.EXPIRED: case PromotionStatus.EXPIRED:
return <StatusDot title="Expired" variant="danger" /> return (
<StatusDot
title={t("discount-table-expired", "Expired")}
variant="danger"
/>
)
case PromotionStatus.ACTIVE: case PromotionStatus.ACTIVE:
return <StatusDot title="Active" variant="success" /> return (
<StatusDot
title={t("discount-table-active", "Active")}
variant="success"
/>
)
case PromotionStatus.DISABLED: case PromotionStatus.DISABLED:
return <StatusDot title="Disabled" variant="default" /> return (
<StatusDot
title={t("discount-table-disabled", "Disabled")}
variant="default"
/>
)
default: default:
return <StatusDot title="Disabled" variant="default" /> return (
<StatusDot
title={t("discount-table-disabled", "Disabled")}
variant="default"
/>
)
} }
} }
@@ -60,7 +86,7 @@ const getCurrencySymbol = (promotion) => {
return "" return ""
} }
const getPromotionAmount = (promotion) => { const getPromotionAmount = (promotion, t) => {
switch (promotion.rule.type) { switch (promotion.rule.type) {
case "fixed": case "fixed":
if (!promotion.regions?.length) { if (!promotion.regions?.length) {
@@ -73,17 +99,18 @@ const getPromotionAmount = (promotion) => {
case "percentage": case "percentage":
return `${promotion.rule.value}%` return `${promotion.rule.value}%`
case "free_shipping": case "free_shipping":
return "Free Shipping" return t("discount-table-free-shipping", "Free Shipping")
default: default:
return "" return ""
} }
} }
export const usePromotionTableColumns = () => { export const usePromotionTableColumns = () => {
const { t } = useTranslation()
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
Header: <div className="pl-2">Code</div>, Header: <div className="pl-2">{t("discount-table-code", "Code")}</div>,
accessor: "code", accessor: "code",
Cell: ({ cell: { value } }) => ( Cell: ({ cell: { value } }) => (
<div className="overflow-hidden"> <div className="overflow-hidden">
@@ -94,16 +121,20 @@ export const usePromotionTableColumns = () => {
), ),
}, },
{ {
Header: "Description", Header: t("discount-table-description", "Description"),
accessor: "rule.description", accessor: "rule.description",
Cell: ({ cell: { value } }) => value, Cell: ({ cell: { value } }) => value,
}, },
{ {
Header: <div className="text-right">Amount</div>, Header: (
<div className="text-right">
{t("discount-table-amount", "Amount")}
</div>
),
id: "amount", id: "amount",
Cell: ({ row: { original } }) => { Cell: ({ row: { original } }) => {
return ( return (
<div className="text-right">{getPromotionAmount(original)}</div> <div className="text-right">{getPromotionAmount(original, t)}</div>
) )
}, },
}, },
@@ -115,14 +146,18 @@ export const usePromotionTableColumns = () => {
), ),
}, },
{ {
Header: "Status", Header: t("discount-table-status", "Status"),
accessor: "ends_at", accessor: "ends_at",
Cell: ({ row: { original } }) => ( Cell: ({ row: { original } }) => (
<div>{getPromotionStatusDot(original)}</div> <div>{getPromotionStatusDot(original, t)}</div>
), ),
}, },
{ {
Header: () => <div className="text-right">Redemptions</div>, Header: () => (
<div className="text-right">
{t("discount-table-redemptions", "Redemptions")}
</div>
),
accessor: "usage_count", accessor: "usage_count",
Cell: ({ row: { original } }) => { Cell: ({ row: { original } }) => {
return ( return (
@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next"
import { useAdminDeleteDiscount, useAdminUpdateDiscount } from "medusa-react" import { useAdminDeleteDiscount, useAdminUpdateDiscount } from "medusa-react"
import useImperativeDialog from "../../../hooks/use-imperative-dialog" import useImperativeDialog from "../../../hooks/use-imperative-dialog"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
@@ -11,6 +12,7 @@ import useCopyPromotion from "./use-copy-promotion"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
const usePromotionActions = (promotion) => { const usePromotionActions = (promotion) => {
const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const notification = useNotification() const notification = useNotification()
const dialog = useImperativeDialog() const dialog = useImperativeDialog()
@@ -22,8 +24,11 @@ const usePromotionActions = (promotion) => {
const handleDelete = async () => { const handleDelete = async () => {
const shouldDelete = await dialog({ const shouldDelete = await dialog({
heading: "Delete Discount", heading: t("discount-table-delete-discount", "Delete Discount"),
text: "Are you sure you want to delete this Discount?", text: t(
"discount-table-confirm-delete",
"Are you sure you want to delete this Discount?"
),
}) })
if (shouldDelete) { if (shouldDelete) {
@@ -39,7 +44,9 @@ const usePromotionActions = (promotion) => {
onClick: () => navigate(`/a/discounts/${promotion.id}`), onClick: () => navigate(`/a/discounts/${promotion.id}`),
}, },
{ {
label: promotion.is_disabled ? "Publish" : "Unpublish", label: promotion.is_disabled
? t("discount-table-publish", "Publish")
: t("discount-table-unpublish", "Unpublish"),
icon: promotion.is_disabled ? ( icon: promotion.is_disabled ? (
<PublishIcon size={20} /> <PublishIcon size={20} />
) : ( ) : (
@@ -53,26 +60,36 @@ const usePromotionActions = (promotion) => {
{ {
onSuccess: () => { onSuccess: () => {
notification( notification(
"Success", t("discount-table-success", "Success"),
`Successfully ${ promotion.is_disabled
promotion.is_disabled ? "published" : "unpublished" ? t(
} discount`, "discount-table-successfully-published-discount",
"Successfully published discount"
)
: t(
"discount-table-successfully-unpublished-discount",
"Successfully unpublished discount"
),
"success" "success"
) )
}, },
onError: (err) => onError: (err) =>
notification("Error", getErrorMessage(err), "error"), notification(
t("discount-table-error", "Error"),
getErrorMessage(err),
"error"
),
} }
) )
}, },
}, },
{ {
label: "Duplicate", label: t("discount-table-duplicate", "Duplicate"),
icon: <DuplicateIcon size={20} />, icon: <DuplicateIcon size={20} />,
onClick: () => copyPromotion(promotion), onClick: () => copyPromotion(promotion),
}, },
{ {
label: "Delete", label: t("discount-table-delete", "Delete"),
icon: <TrashIcon size={20} />, icon: <TrashIcon size={20} />,
variant: "danger", variant: "danger",
onClick: handleDelete, onClick: handleDelete,
@@ -2,6 +2,7 @@ import { useAdminDraftOrders } from "medusa-react"
import { Fragment, useEffect, useState } from "react" import { Fragment, useEffect, useState } from "react"
import { useLocation } from "react-router-dom" import { useLocation } from "react-router-dom"
import { usePagination, useTable } from "react-table" import { usePagination, useTable } from "react-table"
import { useTranslation } from "react-i18next"
import Table from "../../molecules/table" import Table from "../../molecules/table"
import TableContainer from "../../organisms/table-container" import TableContainer from "../../organisms/table-container"
import useDraftOrderTableColumns from "./use-draft-order-column" import useDraftOrderTableColumns from "./use-draft-order-column"
@@ -11,6 +12,7 @@ const DEFAULT_PAGE_SIZE = 15
const DraftOrderTable = () => { const DraftOrderTable = () => {
const location = useLocation() const location = useLocation()
const { t } = useTranslation()
const { const {
reset, reset,
@@ -107,7 +109,7 @@ const DraftOrderTable = () => {
count: count!, count: count!,
offset: queryObject.offset, offset: queryObject.offset,
pageSize: queryObject.offset + rows.length, pageSize: queryObject.offset + rows.length,
title: "Draft Orders", title: t("draft-order-table-draft-orders", "Draft Orders"),
currentPage: pageIndex + 1, currentPage: pageIndex + 1,
pageCount: pageCount, pageCount: pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -1,24 +1,36 @@
import moment from "moment" import moment from "moment"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { getColor } from "../../../utils/color" import { getColor } from "../../../utils/color"
import StatusDot from "../../fundamentals/status-indicator" import StatusDot from "../../fundamentals/status-indicator"
import CustomerAvatarItem from "../../molecules/customer-avatar-item" import CustomerAvatarItem from "../../molecules/customer-avatar-item"
import Table from "../../molecules/table" import Table from "../../molecules/table"
const useDraftOrderTableColumns = () => { const useDraftOrderTableColumns = () => {
const { t } = useTranslation()
const decideStatus = (status) => { const decideStatus = (status) => {
switch (status) { switch (status) {
case "completed": case "completed":
return <StatusDot variant="success" title={"Completed"} /> return (
<StatusDot
variant="success"
title={t("draft-order-table-completed", "Completed")}
/>
)
default: default:
return <StatusDot variant="primary" title={"Open"} /> return (
<StatusDot
variant="primary"
title={t("draft-order-table-open", "Open")}
/>
)
} }
} }
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
Header: "Draft", Header: t("draft-order-table-draft", "Draft"),
accessor: "display_id", accessor: "display_id",
Cell: ({ cell: { value, getCellProps } }) => ( Cell: ({ cell: { value, getCellProps } }) => (
<Table.Cell <Table.Cell
@@ -28,7 +40,7 @@ const useDraftOrderTableColumns = () => {
), ),
}, },
{ {
Header: "Order", Header: t("draft-order-table-order", "Order"),
accessor: "order", accessor: "order",
Cell: ({ cell: { value, getCellProps } }) => { Cell: ({ cell: { value, getCellProps } }) => {
return ( return (
@@ -39,7 +51,7 @@ const useDraftOrderTableColumns = () => {
}, },
}, },
{ {
Header: "Date added", Header: t("draft-order-table-date-added", "Date added"),
accessor: "created_at", accessor: "created_at",
Cell: ({ cell: { value, getCellProps } }) => ( Cell: ({ cell: { value, getCellProps } }) => (
<Table.Cell {...getCellProps()}> <Table.Cell {...getCellProps()}>
@@ -48,7 +60,7 @@ const useDraftOrderTableColumns = () => {
), ),
}, },
{ {
Header: "Customer", Header: t("draft-order-table-customer", "Customer"),
accessor: "cart", accessor: "cart",
Cell: ({ row, cell: { value, getCellProps } }) => ( Cell: ({ row, cell: { value, getCellProps } }) => (
<Table.Cell {...getCellProps()}> <Table.Cell {...getCellProps()}>
@@ -64,7 +76,7 @@ const useDraftOrderTableColumns = () => {
), ),
}, },
{ {
Header: "Status", Header: t("draft-order-table-status", "Status"),
accessor: "status", accessor: "status",
Cell: ({ cell: { value, getCellProps } }) => ( Cell: ({ cell: { value, getCellProps } }) => (
<Table.Cell {...getCellProps()} className="pr-2"> <Table.Cell {...getCellProps()} className="pr-2">
@@ -1,10 +1,12 @@
import clsx from "clsx" import clsx from "clsx"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import FilterDropdownContainer from "../../../components/molecules/filter-dropdown/container" import FilterDropdownContainer from "../../../components/molecules/filter-dropdown/container"
import FilterDropdownItem from "../../../components/molecules/filter-dropdown/item" import FilterDropdownItem from "../../../components/molecules/filter-dropdown/item"
import SaveFilterItem from "../../../components/molecules/filter-dropdown/save-field" import SaveFilterItem from "../../../components/molecules/filter-dropdown/save-field"
import TabFilter from "../../../components/molecules/filter-tab" import TabFilter from "../../../components/molecules/filter-tab"
import PlusIcon from "../../fundamentals/icons/plus-icon" import PlusIcon from "../../fundamentals/icons/plus-icon"
import { TFunction } from "i18next"
const statusFilters = [ const statusFilters = [
"completed", "completed",
@@ -34,12 +36,12 @@ const fulfillmentFilters = [
"canceled", "canceled",
] ]
const dateFilters = [ const dateFilters = (t: TFunction) => [
"is in the last", t("gift-card-filter-dropdown-is-in-the-last", "is in the last"),
"is older than", t("gift-card-filter-dropdown-is-older-than", "is older than"),
"is after", t("gift-card-filter-dropdown-is-after", "is after"),
"is before", t("gift-card-filter-dropdown-is-before", "is before"),
"is equal to", t("gift-card-filter-dropdown-is-equal-to", "is equal to"),
] ]
const OrderFilters = ({ const OrderFilters = ({
@@ -52,6 +54,7 @@ const OrderFilters = ({
submitFilters, submitFilters,
clearFilters, clearFilters,
}) => { }) => {
const { t } = useTranslation()
const [tempState, setTempState] = useState(filters) const [tempState, setTempState] = useState(filters)
const [name, setName] = useState("") const [name, setName] = useState("")
@@ -114,7 +117,7 @@ const OrderFilters = ({
)} )}
> >
<div className="rounded-rounded bg-grey-5 border-grey-20 inter-small-semibold flex h-6 items-center border px-2"> <div className="rounded-rounded bg-grey-5 border-grey-20 inter-small-semibold flex h-6 items-center border px-2">
Filters {t("gift-card-filter-dropdown-filters", "Filters")}
<div className="text-grey-40 ml-1 flex items-center rounded"> <div className="text-grey-40 ml-1 flex items-center rounded">
<span className="text-violet-60 inter-small-semibold"> <span className="text-violet-60 inter-small-semibold">
{numberOfFilters ? numberOfFilters : "0"} {numberOfFilters ? numberOfFilters : "0"}
@@ -128,29 +131,35 @@ const OrderFilters = ({
} }
> >
<FilterDropdownItem <FilterDropdownItem
filterTitle="Status" filterTitle={t("gift-card-filter-dropdown-status", "Status")}
options={statusFilters} options={statusFilters}
filters={tempState.status.filter} filters={tempState.status.filter}
open={tempState.status.open} open={tempState.status.open}
setFilter={(val) => setSingleFilter("status", val)} setFilter={(val) => setSingleFilter("status", val)}
/> />
<FilterDropdownItem <FilterDropdownItem
filterTitle="Payment Status" filterTitle={t(
"gift-card-filter-dropdown-payment-status",
"Payment Status"
)}
options={paymentFilters} options={paymentFilters}
filters={tempState.payment.filter} filters={tempState.payment.filter}
open={tempState.payment.open} open={tempState.payment.open}
setFilter={(val) => setSingleFilter("payment", val)} setFilter={(val) => setSingleFilter("payment", val)}
/> />
<FilterDropdownItem <FilterDropdownItem
filterTitle="Fulfillment Status" filterTitle={t(
"gift-card-filter-dropdown-fulfillment-status",
"Fulfillment Status"
)}
options={fulfillmentFilters} options={fulfillmentFilters}
filters={tempState.fulfillment.filter} filters={tempState.fulfillment.filter}
open={tempState.fulfillment.open} open={tempState.fulfillment.open}
setFilter={(val) => setSingleFilter("fulfillment", val)} setFilter={(val) => setSingleFilter("fulfillment", val)}
/> />
<FilterDropdownItem <FilterDropdownItem
filterTitle="Date" filterTitle={t("gift-card-filter-dropdown-date", "Date")}
options={dateFilters} options={dateFilters(t)}
filters={tempState.date.filter} filters={tempState.date.filter}
open={tempState.date.open} open={tempState.date.open}
setFilter={(val) => setSingleFilter("date", val)} setFilter={(val) => setSingleFilter("date", val)}
@@ -5,6 +5,7 @@ import qs from "qs"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useLocation } from "react-router-dom" import { useLocation } from "react-router-dom"
import { usePagination, useTable } from "react-table" import { usePagination, useTable } from "react-table"
import { useTranslation } from "react-i18next"
import Spinner from "../../atoms/spinner" import Spinner from "../../atoms/spinner"
import Table from "../../molecules/table" import Table from "../../molecules/table"
import TableContainer from "../../organisms/table-container" import TableContainer from "../../organisms/table-container"
@@ -17,6 +18,7 @@ const defaultQueryProps = {}
const GiftCardTable = () => { const GiftCardTable = () => {
const location = useLocation() const location = useLocation()
const { t } = useTranslation()
const { const {
reset, reset,
@@ -130,7 +132,7 @@ const GiftCardTable = () => {
count: count!, count: count!,
offset: queryObject.offset, offset: queryObject.offset,
pageSize: queryObject.offset + rows.length, pageSize: queryObject.offset + rows.length,
title: "Gift cards", title: t("gift-card-table-gift-cards", "Gift cards"),
currentPage: pageIndex + 1, currentPage: pageIndex + 1,
pageCount: pageCount, pageCount: pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -1,15 +1,17 @@
import moment from "moment" import moment from "moment"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { formatAmountWithSymbol } from "../../../utils/prices" import { formatAmountWithSymbol } from "../../../utils/prices"
import StatusIndicator from "../../fundamentals/status-indicator" import StatusIndicator from "../../fundamentals/status-indicator"
import IconTooltip from "../../molecules/icon-tooltip" import IconTooltip from "../../molecules/icon-tooltip"
import Table from "../../molecules/table" import Table from "../../molecules/table"
const useGiftCardTableColums = () => { const useGiftCardTableColums = () => {
const { t } = useTranslation()
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
Header: <div className="pl-2">Code</div>, Header: <div className="pl-2">{t("gift-card-table-code", "Code")}</div>,
accessor: "code", accessor: "code",
Cell: ({ cell: { value }, index }) => ( Cell: ({ cell: { value }, index }) => (
<Table.Cell <Table.Cell
@@ -21,7 +23,7 @@ const useGiftCardTableColums = () => {
), ),
}, },
{ {
Header: "Order", Header: t("gift-card-table-order", "Order"),
accessor: "order", accessor: "order",
Cell: ({ cell: { value }, index }) => ( Cell: ({ cell: { value }, index }) => (
<Table.Cell <Table.Cell
@@ -37,7 +39,7 @@ const useGiftCardTableColums = () => {
), ),
}, },
{ {
Header: "Original Amount", Header: t("gift-card-table-original-amount", "Original Amount"),
accessor: "value", accessor: "value",
Cell: ({ row, cell: { value }, index }) => ( Cell: ({ row, cell: { value }, index }) => (
<Table.Cell key={index}> <Table.Cell key={index}>
@@ -56,7 +58,7 @@ const useGiftCardTableColums = () => {
), ),
}, },
{ {
Header: "Balance", Header: t("gift-card-table-balance", "Balance"),
accessor: "balance", accessor: "balance",
Cell: ({ row, cell: { value }, index }) => ( Cell: ({ row, cell: { value }, index }) => (
<Table.Cell key={index}> <Table.Cell key={index}>
@@ -69,11 +71,19 @@ const useGiftCardTableColums = () => {
) : ( ) : (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span>N / A</span> <span>N / A</span>
<IconTooltip content={"Region has been deleted"} /> <IconTooltip
content={t(
"gift-card-table-region-has-been-deleted",
"Region has been deleted"
)}
/>
</div> </div>
) )
) : ( ) : (
<StatusIndicator title="None" variant="danger" /> <StatusIndicator
title={t("gift-card-table-none", "None")}
variant="danger"
/>
)} )}
</Table.Cell> </Table.Cell>
), ),
@@ -81,7 +91,7 @@ const useGiftCardTableColums = () => {
{ {
Header: () => ( Header: () => (
<div className="rounded-rounded flex w-full justify-end pr-2"> <div className="rounded-rounded flex w-full justify-end pr-2">
Created {t("gift-card-table-created", "Created")}
</div> </div>
), ),
accessor: "created_at", accessor: "created_at",
@@ -1,6 +1,7 @@
import { useMemo } from "react" import { useMemo } from "react"
import { Controller } from "react-hook-form" import { Controller } from "react-hook-form"
import { Column, useTable } from "react-table" import { Column, useTable } from "react-table"
import { useTranslation } from "react-i18next"
import { FormImage } from "../../../types/shared" import { FormImage } from "../../../types/shared"
import { NestedForm } from "../../../utils/nested-form" import { NestedForm } from "../../../utils/nested-form"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
@@ -18,6 +19,7 @@ type ImageTableProps = {
} }
const ImageTable = ({ data, form, onDelete }: ImageTableProps) => { const ImageTable = ({ data, form, onDelete }: ImageTableProps) => {
const { t } = useTranslation()
const { control, register, path } = form const { control, register, path } = form
const columns = useMemo< const columns = useMemo<
@@ -46,7 +48,7 @@ const ImageTable = ({ data, form, onDelete }: ImageTableProps) => {
}, },
}, },
{ {
Header: () => <span>File name</span>, Header: () => <span>{t("image-table-file-name", "File name")}</span>,
accessor: "nativeFile", accessor: "nativeFile",
Cell: ({ cell }) => { Cell: ({ cell }) => {
return ( return (
@@ -72,8 +74,13 @@ const ImageTable = ({ data, form, onDelete }: ImageTableProps) => {
{ {
Header: () => ( Header: () => (
<div className="flex items-center justify-center gap-x-[6px]"> <div className="flex items-center justify-center gap-x-[6px]">
<span>Thumbnail</span> <span>{t("image-table-thumbnail", "Thumbnail")}</span>
<IconTooltip content="Select which image you want to use as the thumbnail for this product" /> <IconTooltip
content={t(
"image-table-select-thumbnail-image-for-product",
"Select which image you want to use as the thumbnail for this product"
)}
/>
</div> </div>
), ),
id: "thumbnail", id: "thumbnail",
@@ -13,6 +13,7 @@ import {
useAdminVariant, useAdminVariant,
} from "medusa-react" } from "medusa-react"
import { useLocation, useNavigate } from "react-router-dom" import { useLocation, useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import ImagePlaceholder from "../../fundamentals/image-placeholder" import ImagePlaceholder from "../../fundamentals/image-placeholder"
@@ -84,6 +85,7 @@ const InventoryTable: React.FC<InventoryTableProps> = () => {
const { store } = useAdminStore() const { store } = useAdminStore()
const location = useLocation() const location = useLocation()
const { t } = useTranslation()
const { stock_locations, isLoading: locationsLoading } = const { stock_locations, isLoading: locationsLoading } =
useAdminStockLocations() useAdminStockLocations()
@@ -217,7 +219,7 @@ const InventoryTable: React.FC<InventoryTableProps> = () => {
count: count || 0, count: count || 0,
offset: offs, offset: offs,
pageSize: offs + rows.length, pageSize: offs + rows.length,
title: "Inventory Items", title: t("inventory-table-inventory-items", "Inventory Items"),
currentPage: pageIndex + 1, currentPage: pageIndex + 1,
pageCount: pageCount, pageCount: pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -300,6 +302,7 @@ const InventoryRow = ({
const inventory = row.original const inventory = row.original
const navigate = useNavigate() const navigate = useNavigate()
const { t } = useTranslation()
const { const {
state: isShowingAdjustAvailabilityModal, state: isShowingAdjustAvailabilityModal,
@@ -312,7 +315,10 @@ const InventoryRow = ({
const actions = [ const actions = [
{ {
label: "Adjust Availability", label: t(
"inventory-table-actions-adjust-availability",
"Adjust Availability"
),
onClick: showAdjustAvailabilityModal, onClick: showAdjustAvailabilityModal,
}, },
] ]
@@ -320,7 +326,7 @@ const InventoryRow = ({
if (productId) { if (productId) {
return [ return [
{ {
label: "View Product", label: t("inventory-table-view-product", "View Product"),
onClick: () => navigate(`/a/products/${productId}`), onClick: () => navigate(`/a/products/${productId}`),
}, },
...actions, ...actions,
@@ -370,6 +376,7 @@ const AdjustAvailabilityModal = ({
const inventoryVariantId = inventory.variants?.[0]?.id const inventoryVariantId = inventory.variants?.[0]?.id
const locationLevel = inventory.location_levels?.[0] const locationLevel = inventory.location_levels?.[0]
const { t } = useTranslation()
const { variant, isLoading } = useAdminVariant(inventoryVariantId || "") const { variant, isLoading } = useAdminVariant(inventoryVariantId || "")
const { const {
mutate: updateLocationLevelForInventoryItem, mutate: updateLocationLevelForInventoryItem,
@@ -396,8 +403,11 @@ const AdjustAvailabilityModal = ({
{ {
onSuccess: () => { onSuccess: () => {
notification( notification(
"Success", t("inventory-table-success", "Success"),
"Inventory item updated successfully", t(
"inventory-table-inventory-item-updated-successfully",
"Inventory item updated successfully"
),
"success" "success"
) )
handleClose() handleClose()
@@ -412,7 +422,9 @@ const AdjustAvailabilityModal = ({
<Modal handleClose={handleClose}> <Modal handleClose={handleClose}>
<Modal.Body> <Modal.Body>
<Modal.Header handleClose={handleClose}> <Modal.Header handleClose={handleClose}>
<h1 className="inter-large-semibold">Adjust availability</h1> <h1 className="inter-large-semibold">
{t("inventory-table-adjust-availability", "Adjust availability")}
</h1>
</Modal.Header> </Modal.Header>
<Modal.Content> <Modal.Content>
{isLoading ? ( {isLoading ? (
@@ -468,7 +480,7 @@ const AdjustAvailabilityModal = ({
className="border" className="border"
onClick={handleClose} onClick={handleClose}
> >
Cancel {t("inventory-table-cancel", "Cancel")}
</Button> </Button>
<Button <Button
size="small" size="small"
@@ -477,7 +489,7 @@ const AdjustAvailabilityModal = ({
loading={isSubmitting} loading={isSubmitting}
onClick={onSubmit} onClick={onSubmit}
> >
Save and close {t("inventory-table-save-and-close", "Save and close")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -6,16 +6,18 @@ import { InventoryLevelDTO } from "@medusajs/types"
import Tooltip from "../../atoms/tooltip" import Tooltip from "../../atoms/tooltip"
import { useMemo } from "react" import { useMemo } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
const useInventoryTableColumn = ({ const useInventoryTableColumn = ({
location_id, location_id,
}: { }: {
location_id: string location_id: string
}): [Column<DecoratedInventoryItemDTO>[]] => { }): [Column<DecoratedInventoryItemDTO>[]] => {
const { t } = useTranslation()
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
Header: "Item", Header: t("inventory-table-item", "Item"),
accessor: "title", accessor: "title",
Cell: ({ row: { original } }) => { Cell: ({ row: { original } }) => {
return ( return (
@@ -36,18 +38,18 @@ const useInventoryTableColumn = ({
}, },
}, },
{ {
Header: "Variant", Header: t("inventory-table-variant", "Variant"),
Cell: ({ row: { original } }) => { Cell: ({ row: { original } }) => {
return <div>{original?.variants[0]?.title || "-"}</div> return <div>{original?.variants[0]?.title || "-"}</div>
}, },
}, },
{ {
Header: "SKU", Header: t("inventory-table-sku", "SKU"),
accessor: "sku", accessor: "sku",
Cell: ({ cell: { value } }) => value, Cell: ({ cell: { value } }) => value,
}, },
{ {
Header: "Reserved", Header: t("inventory-table-reserved", "Reserved"),
accessor: "reserved_quantity", accessor: "reserved_quantity",
Cell: ({ row: { original } }) => { Cell: ({ row: { original } }) => {
const navigate = useNavigate() const navigate = useNavigate()
@@ -85,7 +87,7 @@ const useInventoryTableColumn = ({
}, },
}, },
{ {
Header: "In stock", Header: t("inventory-table-in-stock", "In stock"),
accessor: "stocked_quantity", accessor: "stocked_quantity",
Cell: ({ row: { original } }) => { Cell: ({ row: { original } }) => {
return ( return (
@@ -1,12 +1,12 @@
import clsx from "clsx" import clsx from "clsx"
import { useAdminRegions, useAdminSalesChannels } from "medusa-react" import { useAdminRegions, useAdminSalesChannels } from "medusa-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import FilterDropdownContainer from "../../../components/molecules/filter-dropdown/container" import FilterDropdownContainer from "../../../components/molecules/filter-dropdown/container"
import FilterDropdownItem from "../../../components/molecules/filter-dropdown/item" import FilterDropdownItem from "../../../components/molecules/filter-dropdown/item"
import SaveFilterItem from "../../../components/molecules/filter-dropdown/save-field" import SaveFilterItem from "../../../components/molecules/filter-dropdown/save-field"
import TabFilter from "../../../components/molecules/filter-tab" import TabFilter from "../../../components/molecules/filter-tab"
import PlusIcon from "../../fundamentals/icons/plus-icon" import PlusIcon from "../../fundamentals/icons/plus-icon"
import FeatureToggle from "../../fundamentals/feature-toggle"
import { useFeatureFlag } from "../../../providers/feature-flag-provider" import { useFeatureFlag } from "../../../providers/feature-flag-provider"
const REGION_PAGE_SIZE = 10 const REGION_PAGE_SIZE = 10
@@ -58,6 +58,7 @@ const OrderFilters = ({
submitFilters, submitFilters,
clearFilters, clearFilters,
}) => { }) => {
const { t } = useTranslation()
const [tempState, setTempState] = useState(filters) const [tempState, setTempState] = useState(filters)
const [name, setName] = useState("") const [name, setName] = useState("")
@@ -154,7 +155,7 @@ const OrderFilters = ({
)} )}
> >
<div className="rounded-rounded bg-grey-5 border-grey-20 inter-small-semibold flex h-6 items-center border px-2"> <div className="rounded-rounded bg-grey-5 border-grey-20 inter-small-semibold flex h-6 items-center border px-2">
Filters {t("order-filter-dropdown-filters", "Filters")}
<div className="text-grey-40 ml-1 flex items-center rounded"> <div className="text-grey-40 ml-1 flex items-center rounded">
<span className="text-violet-60 inter-small-semibold"> <span className="text-violet-60 inter-small-semibold">
{numberOfFilters ? numberOfFilters : "0"} {numberOfFilters ? numberOfFilters : "0"}
@@ -168,28 +169,34 @@ const OrderFilters = ({
} }
> >
<FilterDropdownItem <FilterDropdownItem
filterTitle="Status" filterTitle={t("order-filter-dropdown-status", "Status")}
options={statusFilters} options={statusFilters}
filters={tempState.status.filter} filters={tempState.status.filter}
open={tempState.status.open} open={tempState.status.open}
setFilter={(val) => setSingleFilter("status", val)} setFilter={(val) => setSingleFilter("status", val)}
/> />
<FilterDropdownItem <FilterDropdownItem
filterTitle="Payment Status" filterTitle={t(
"order-filter-dropdown-payment-status",
"Payment Status"
)}
options={paymentFilters} options={paymentFilters}
filters={tempState.payment.filter} filters={tempState.payment.filter}
open={tempState.payment.open} open={tempState.payment.open}
setFilter={(val) => setSingleFilter("payment", val)} setFilter={(val) => setSingleFilter("payment", val)}
/> />
<FilterDropdownItem <FilterDropdownItem
filterTitle="Fulfillment Status" filterTitle={t(
"order-filter-dropdown-fulfillment-status",
"Fulfillment Status"
)}
options={fulfillmentFilters} options={fulfillmentFilters}
filters={tempState.fulfillment.filter} filters={tempState.fulfillment.filter}
open={tempState.fulfillment.open} open={tempState.fulfillment.open}
setFilter={(val) => setSingleFilter("fulfillment", val)} setFilter={(val) => setSingleFilter("fulfillment", val)}
/> />
<FilterDropdownItem <FilterDropdownItem
filterTitle="Regions" filterTitle={t("order-filter-dropdown-regions", "Regions")}
options={ options={
regions?.map((region) => ({ regions?.map((region) => ({
value: region.id, value: region.id,
@@ -209,7 +216,10 @@ const OrderFilters = ({
/> />
{isSalesChannelsEnabled && ( {isSalesChannelsEnabled && (
<FilterDropdownItem <FilterDropdownItem
filterTitle="Sales Channel" filterTitle={t(
"order-filter-dropdown-sales-channel",
"Sales Channel"
)}
options={ options={
sales_channels?.map((salesChannel) => ({ sales_channels?.map((salesChannel) => ({
value: salesChannel.id, value: salesChannel.id,
@@ -223,7 +233,7 @@ const OrderFilters = ({
/> />
)} )}
<FilterDropdownItem <FilterDropdownItem
filterTitle="Date" filterTitle={t("order-filter-dropdown-date", "Date")}
options={dateFilters} options={dateFilters}
filters={tempState.date.filter} filters={tempState.date.filter}
open={tempState.date.open} open={tempState.date.open}
@@ -1,6 +1,7 @@
import moment from "moment" import moment from "moment"
import { useMemo } from "react" import { useMemo } from "react"
import ReactCountryFlag from "react-country-flag" import ReactCountryFlag from "react-country-flag"
import { useTranslation } from "react-i18next"
import { getColor } from "../../../utils/color" import { getColor } from "../../../utils/color"
import { isoAlpha2Countries } from "../../../utils/countries" import { isoAlpha2Countries } from "../../../utils/countries"
import { formatAmountWithSymbol } from "../../../utils/prices" import { formatAmountWithSymbol } from "../../../utils/prices"
@@ -9,32 +10,52 @@ import StatusDot from "../../fundamentals/status-indicator"
import CustomerAvatarItem from "../../molecules/customer-avatar-item" import CustomerAvatarItem from "../../molecules/customer-avatar-item"
const useOrderTableColums = () => { const useOrderTableColums = () => {
const { t } = useTranslation()
const decideStatus = (status) => { const decideStatus = (status) => {
switch (status) { switch (status) {
case "captured": case "captured":
return <StatusDot variant="success" title={"Paid"} /> return (
<StatusDot variant="success" title={t("order-table-paid", "Paid")} />
)
case "awaiting": case "awaiting":
return <StatusDot variant="default" title={"Awaiting"} /> return (
<StatusDot
variant="default"
title={t("order-table-awaiting", "Awaiting")}
/>
)
case "requires_action": case "requires_action":
return <StatusDot variant="danger" title={"Requires action"} /> return (
<StatusDot
variant="danger"
title={t("order-table-requires-action", "Requires action")}
/>
)
case "canceled": case "canceled":
return <StatusDot variant="warning" title={"Canceled"} /> return (
<StatusDot
variant="warning"
title={t("order-table-canceled", "Canceled")}
/>
)
default: default:
return <StatusDot variant="primary" title={"N/A"} /> return (
<StatusDot variant="primary" title={t("order-table-n-a", "N/A")} />
)
} }
} }
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
Header: <div className="pl-2">Order</div>, Header: <div className="pl-2">{t("order-table-order", "Order")}</div>,
accessor: "display_id", accessor: "display_id",
Cell: ({ cell: { value } }) => ( Cell: ({ cell: { value } }) => (
<p className="text-grey-90 group-hover:text-violet-60 min-w-[100px] pl-2">{`#${value}`}</p> <p className="text-grey-90 group-hover:text-violet-60 min-w-[100px] pl-2">{`#${value}`}</p>
), ),
}, },
{ {
Header: "Date added", Header: t("order-table-date-added", "Date added"),
accessor: "created_at", accessor: "created_at",
Cell: ({ cell: { value } }) => ( Cell: ({ cell: { value } }) => (
<div> <div>
@@ -45,7 +66,7 @@ const useOrderTableColums = () => {
), ),
}, },
{ {
Header: "Customer", Header: t("order-table-customer", "Customer"),
accessor: "customer", accessor: "customer",
Cell: ({ row, cell: { value } }) => ( Cell: ({ row, cell: { value } }) => (
<div> <div>
@@ -64,22 +85,24 @@ const useOrderTableColums = () => {
), ),
}, },
{ {
Header: "Fulfillment", Header: t("order-table-fulfillment", "Fulfillment"),
accessor: "fulfillment_status", accessor: "fulfillment_status",
Cell: ({ cell: { value } }) => value, Cell: ({ cell: { value } }) => value,
}, },
{ {
Header: "Payment status", Header: t("order-table-payment-status", "Payment status"),
accessor: "payment_status", accessor: "payment_status",
Cell: ({ cell: { value } }) => decideStatus(value), Cell: ({ cell: { value } }) => decideStatus(value),
}, },
{ {
Header: "Sales Channel", Header: t("order-table-sales-channel", "Sales Channel"),
accessor: "sales_channel", accessor: "sales_channel",
Cell: ({ cell: { value } }) => value?.name ?? "N/A", Cell: ({ cell: { value } }) => value?.name ?? "N/A",
}, },
{ {
Header: () => <div className="text-right">Total</div>, Header: () => (
<div className="text-right">{t("order-table-total", "Total")}</div>
),
accessor: "total", accessor: "total",
Cell: ({ row, cell: { value } }) => ( Cell: ({ row, cell: { value } }) => (
<div className="text-right"> <div className="text-right">
@@ -1,6 +1,7 @@
import { omit } from "lodash" import { omit } from "lodash"
import qs from "qs" import qs from "qs"
import { useMemo, useReducer, useState } from "react" import { useMemo, useReducer, useState } from "react"
import { useTranslation } from "react-i18next"
import { relativeDateFormatToTimestamp } from "../../../utils/time" import { relativeDateFormatToTimestamp } from "../../../utils/time"
type OrderDateFilter = null | { type OrderDateFilter = null | {
@@ -156,6 +157,8 @@ export const useOrderFilters = (
existing?: string, existing?: string,
defaultFilters: OrderDefaultFilters | null = null defaultFilters: OrderDefaultFilters | null = null
) => { ) => {
const { t } = useTranslation()
if (existing && existing[0] === "?") { if (existing && existing[0] === "?") {
existing = existing.substring(1) existing = existing.substring(1)
} }
@@ -365,11 +368,11 @@ export const useOrderFilters = (
const availableTabs = useMemo(() => { const availableTabs = useMemo(() => {
return [ return [
{ {
label: "Complete", label: t("order-table-filters-complete", "Complete"),
value: "complete", value: "complete",
}, },
{ {
label: "Incomplete", label: t("order-table-filters-incomplete", "Incomplete"),
value: "incomplete", value: "incomplete",
}, },
...tabs, ...tabs,
@@ -423,7 +426,6 @@ export const useOrderFilters = (
const clean = omit(repObj, ["limit", "offset"]) const clean = omit(repObj, ["limit", "offset"])
const repString = qs.stringify(clean, { skipNulls: true }) const repString = qs.stringify(clean, { skipNulls: true })
const storedString = localStorage.getItem("orders::filters") const storedString = localStorage.getItem("orders::filters")
let existing: null | object = null let existing: null | object = null
@@ -1,5 +1,6 @@
import clsx from "clsx" import clsx from "clsx"
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import PlusIcon from "../../../components/fundamentals/icons/plus-icon" import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
import FilterDropdownContainer from "../../../components/molecules/filter-dropdown/container" import FilterDropdownContainer from "../../../components/molecules/filter-dropdown/container"
import FilterDropdownItem from "../../../components/molecules/filter-dropdown/item" import FilterDropdownItem from "../../../components/molecules/filter-dropdown/item"
@@ -19,6 +20,7 @@ const PriceListsFilter = ({
onRemoveTab, onRemoveTab,
onSaveTab, onSaveTab,
}) => { }) => {
const { t } = useTranslation()
const [tempState, setTempState] = useState(filters) const [tempState, setTempState] = useState(filters)
const [name, setName] = useState("") const [name, setName] = useState("")
@@ -82,7 +84,7 @@ const PriceListsFilter = ({
)} )}
> >
<div className="rounded-rounded bg-grey-5 border-grey-20 inter-small-semibold flex h-6 items-center border px-2"> <div className="rounded-rounded bg-grey-5 border-grey-20 inter-small-semibold flex h-6 items-center border px-2">
Filters {t("price-list-table-filters", "Filters")}
<div className="text-grey-40 ml-1 flex items-center rounded"> <div className="text-grey-40 ml-1 flex items-center rounded">
<span className="text-violet-60 inter-small-semibold"> <span className="text-violet-60 inter-small-semibold">
{numberOfFilters ? numberOfFilters : "0"} {numberOfFilters ? numberOfFilters : "0"}
@@ -96,14 +98,14 @@ const PriceListsFilter = ({
} }
> >
<FilterDropdownItem <FilterDropdownItem
filterTitle="Status" filterTitle={t("price-list-table-status", "Status")}
options={statusFilters} options={statusFilters}
filters={tempState.status.filter} filters={tempState.status.filter}
open={tempState.status.open} open={tempState.status.open}
setFilter={(v) => setSingleFilter("status", v)} setFilter={(v) => setSingleFilter("status", v)}
/> />
<FilterDropdownItem <FilterDropdownItem
filterTitle="Type" filterTitle={t("price-list-table-type", "Type")}
options={typeFilters} options={typeFilters}
filters={tempState.type.filter} filters={tempState.type.filter}
open={tempState.type.open} open={tempState.type.open}
@@ -11,6 +11,7 @@ import {
UseSortByColumnProps, UseSortByColumnProps,
useTable, useTable,
} from "react-table" } from "react-table"
import { useTranslation } from "react-i18next"
import { useDebounce } from "../../../hooks/use-debounce" import { useDebounce } from "../../../hooks/use-debounce"
import Table, { TableProps } from "../../molecules/table" import Table, { TableProps } from "../../molecules/table"
import TableContainer from "../../organisms/table-container" import TableContainer from "../../organisms/table-container"
@@ -121,6 +122,7 @@ export function PriceListTable(props: PriceListTableProps) {
autoResetPage: false, autoResetPage: false,
} }
const { t } = useTranslation()
const table = useTable(tableConfig, useSortBy, usePagination, useRowSelect) const table = useTable(tableConfig, useSortBy, usePagination, useRowSelect)
// ********* HANDLERS ********* // ********* HANDLERS *********
@@ -164,7 +166,7 @@ export function PriceListTable(props: PriceListTableProps) {
count: count!, count: count!,
offset: queryObject.offset, offset: queryObject.offset,
pageSize: queryObject.offset + table.rows.length, pageSize: queryObject.offset + table.rows.length,
title: "Price Lists", title: t("price-list-table-price-lists", "Price Lists"),
currentPage: table.state.pageIndex + 1, currentPage: table.state.pageIndex + 1,
pageCount: table.pageCount, pageCount: table.pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -1,5 +1,6 @@
import { useAdminCreatePriceList } from "medusa-react" import { useAdminCreatePriceList } from "medusa-react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
@@ -7,6 +8,7 @@ const useCopyPriceList = () => {
const navigate = useNavigate() const navigate = useNavigate()
const notification = useNotification() const notification = useNotification()
const createPriceList = useAdminCreatePriceList() const createPriceList = useAdminCreatePriceList()
const { t } = useTranslation()
const handleCopyPriceList = async (priceList) => { const handleCopyPriceList = async (priceList) => {
const copy: any = { const copy: any = {
@@ -44,9 +46,20 @@ const useCopyPriceList = () => {
try { try {
const data = await createPriceList.mutateAsync(copy) const data = await createPriceList.mutateAsync(copy)
navigate(`/a/pricing/${data.price_list.id}`) navigate(`/a/pricing/${data.price_list.id}`)
notification("Success", "Successfully copied price list", "success") notification(
t("price-list-table-success", "Success"),
t(
"price-list-table-successfully-copied-price-list",
"Successfully copied price list"
),
"success"
)
} catch (err) { } catch (err) {
notification("Error", getErrorMessage(err), "error") notification(
t("price-list-table-error", "Error"),
getErrorMessage(err),
"error"
)
} }
} }
@@ -1,4 +1,5 @@
import { useAdminDeletePriceList, useAdminUpdatePriceList } from "medusa-react" import { useAdminDeletePriceList, useAdminUpdatePriceList } from "medusa-react"
import { useTranslation } from "react-i18next"
import useImperativeDialog from "../../../hooks/use-imperative-dialog" import useImperativeDialog from "../../../hooks/use-imperative-dialog"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
@@ -9,6 +10,7 @@ import { isActive } from "./utils"
import PublishIcon from "../../fundamentals/icons/publish-icon" import PublishIcon from "../../fundamentals/icons/publish-icon"
const usePriceListActions = (priceList) => { const usePriceListActions = (priceList) => {
const { t } = useTranslation()
const dialog = useImperativeDialog() const dialog = useImperativeDialog()
const notification = useNotification() const notification = useNotification()
const updatePrice = useAdminUpdatePriceList(priceList?.id) const updatePrice = useAdminUpdatePriceList(priceList?.id)
@@ -16,19 +18,30 @@ const usePriceListActions = (priceList) => {
const onDelete = async () => { const onDelete = async () => {
const shouldDelete = await dialog({ const shouldDelete = await dialog({
heading: "Delete Price List", heading: t("price-list-table-delete-price-list", "Delete Price List"),
text: "Are you sure you want to delete this price list?", text: t(
"price-list-table-confirm-delete",
"Are you sure you want to delete this price list?"
),
}) })
if (shouldDelete) { if (shouldDelete) {
deletePrice.mutate(undefined, { deletePrice.mutate(undefined, {
onSuccess: () => { onSuccess: () => {
notification( notification(
"Success", t("price-list-table-success", "Success"),
"Successfully deleted the price list", t(
"price-list-table-successfully-deleted-the-price-list",
"Successfully deleted the price list"
),
"success" "success"
) )
}, },
onError: (err) => notification("Error", getErrorMessage(err), "error"), onError: (err) =>
notification(
t("price-list-table-error", "Error"),
getErrorMessage(err),
"error"
),
}) })
} }
} }
@@ -41,10 +54,16 @@ const usePriceListActions = (priceList) => {
{ {
onSuccess: () => { onSuccess: () => {
notification( notification(
"Success", t("price-list-table-success", "Success"),
`Successfully ${ isActive(priceList)
isActive(priceList) ? "unpublished" : "published" ? t(
} price list`, "price-list-table-successfully-unpublished-price-list",
"Successfully unpublished price list"
)
: t(
"price-list-table-successfully-published-price-list",
"Successfully published price list"
),
"success" "success"
) )
}, },
@@ -54,7 +73,9 @@ const usePriceListActions = (priceList) => {
const getActions = (): ActionType[] => [ const getActions = (): ActionType[] => [
{ {
label: isActive(priceList) ? "Unpublish" : "Publish", label: isActive(priceList)
? t("price-list-table-unpublish", "Unpublish")
: t("price-list-table-publish", "Publish"),
onClick: onUpdate, onClick: onUpdate,
icon: isActive(priceList) ? ( icon: isActive(priceList) ? (
<UnpublishIcon size={20} /> <UnpublishIcon size={20} />
@@ -63,7 +84,7 @@ const usePriceListActions = (priceList) => {
), ),
}, },
{ {
label: "Delete", label: t("price-list-table-delete", "Delete"),
onClick: onDelete, onClick: onDelete,
icon: <TrashIcon size={20} />, icon: <TrashIcon size={20} />,
variant: "danger", variant: "danger",
@@ -2,16 +2,18 @@ import { PriceList } from "@medusajs/medusa"
import { isArray } from "lodash" import { isArray } from "lodash"
import { useMemo } from "react" import { useMemo } from "react"
import { Column } from "react-table" import { Column } from "react-table"
import { useTranslation } from "react-i18next"
import Actionables from "../../molecules/actionables" import Actionables from "../../molecules/actionables"
import Table from "../../molecules/table" import Table from "../../molecules/table"
import usePriceListActions from "./use-price-list-actions" import usePriceListActions from "./use-price-list-actions"
import { formatPriceListGroups, getPriceListStatus } from "./utils" import { formatPriceListGroups, getPriceListStatus } from "./utils"
export const usePriceListTableColumns = () => { export const usePriceListTableColumns = () => {
const { t } = useTranslation()
const columns = useMemo<Column<PriceList>[]>( const columns = useMemo<Column<PriceList>[]>(
() => [ () => [
{ {
Header: "Name", Header: t("price-list-table-name", "Name"),
accessor: "name", accessor: "name",
Cell: ({ cell: { value } }) => ( Cell: ({ cell: { value } }) => (
<Table.Cell> <Table.Cell>
@@ -20,19 +22,19 @@ export const usePriceListTableColumns = () => {
), ),
}, },
{ {
Header: "Description", Header: t("price-list-table-description", "Description"),
accessor: "description", accessor: "description",
Cell: ({ cell: { value } }) => <Table.Cell>{value}</Table.Cell>, Cell: ({ cell: { value } }) => <Table.Cell>{value}</Table.Cell>,
}, },
{ {
Header: "Status", Header: t("price-list-table-status", "Status"),
accessor: "status", accessor: "status",
Cell: ({ row: { original } }) => ( Cell: ({ row: { original } }) => (
<Table.Cell>{getPriceListStatus(original)}</Table.Cell> <Table.Cell>{getPriceListStatus(original)}</Table.Cell>
), ),
}, },
{ {
Header: "Groups", Header: t("price-list-table-groups", "Groups"),
accessor: "customer_groups", accessor: "customer_groups",
Cell: ({ cell: { value } }) => { Cell: ({ cell: { value } }) => {
const groups: string[] = isArray(value) const groups: string[] = isArray(value)
@@ -42,7 +44,14 @@ export const usePriceListTableColumns = () => {
return ( return (
<Table.Cell> <Table.Cell>
{group} {group}
{other && <span className="text-grey-40"> + {other} more</span>} {other && (
<span className="text-grey-40">
{" "}
{t("price-list-table-other-more", "+ {{other}} more", {
other,
})}
</span>
)}
</Table.Cell> </Table.Cell>
) )
}, },
@@ -1,6 +1,7 @@
import { MoneyAmount, ProductVariant } from "@medusajs/medusa" import { MoneyAmount, ProductVariant } from "@medusajs/medusa"
import React from "react" import React from "react"
import { Control, Controller, useForm, useWatch } from "react-hook-form" import { Control, Controller, useForm, useWatch } from "react-hook-form"
import { useTranslation } from "react-i18next"
import Checkbox, { CheckboxProps } from "../../atoms/checkbox" import Checkbox, { CheckboxProps } from "../../atoms/checkbox"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
import Modal from "../../molecules/modal" import Modal from "../../molecules/modal"
@@ -35,6 +36,7 @@ const PriceOverrides = ({
defaultVariant, defaultVariant,
isEdit = false, isEdit = false,
}: PriceOverridesType) => { }: PriceOverridesType) => {
const { t } = useTranslation()
const [mode, setMode] = React.useState(MODES.SELECTED_ONLY) const [mode, setMode] = React.useState(MODES.SELECTED_ONLY)
const { const {
handleSubmit, handleSubmit,
@@ -92,11 +94,17 @@ const PriceOverrides = ({
> >
<RadioGroup.SimpleItem <RadioGroup.SimpleItem
value={MODES.SELECTED_ONLY} value={MODES.SELECTED_ONLY}
label="Apply overrides on selected variants" label={t(
"price-overrides-apply-overrides-on-selected-variants",
"Apply overrides on selected variants"
)}
/> />
<RadioGroup.SimpleItem <RadioGroup.SimpleItem
value={MODES.APPLY_ALL} value={MODES.APPLY_ALL}
label="Apply on all variants" label={t(
"price-overrides-apply-on-all-variants",
"Apply on all variants"
)}
/> />
</RadioGroup.Root> </RadioGroup.Root>
)} )}
@@ -120,7 +128,9 @@ const PriceOverrides = ({
</div> </div>
)} )}
<div className="pt-8"> <div className="pt-8">
<h6 className="inter-base-semibold">Prices</h6> <h6 className="inter-base-semibold">
{t("price-overrides-prices", "Prices")}
</h6>
<div className="pt-4"> <div className="pt-4">
{prices.map((price, idx) => ( {prices.map((price, idx) => (
<Controller <Controller
@@ -154,7 +164,7 @@ const PriceOverrides = ({
size="large" size="large"
onClick={onClose} onClick={onClose}
> >
Cancel {t("price-overrides-cancel", "Cancel")}
</Button> </Button>
<Button <Button
size="large" size="large"
@@ -163,7 +173,7 @@ const PriceOverrides = ({
onClick={onClick} onClick={onClick}
loading={isSubmitting} loading={isSubmitting}
> >
Save and close {t("price-overrides-save-and-close", "Save and close")}
</Button> </Button>
</div> </div>
</Modal.Footer> </Modal.Footer>
@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next"
import useToggleState from "../../../hooks/use-toggle-state" import useToggleState from "../../../hooks/use-toggle-state"
import { currencies } from "../../../utils/currencies" import { currencies } from "../../../utils/currencies"
import Button from "../../fundamentals/button" import Button from "../../fundamentals/button"
@@ -6,6 +7,7 @@ import EyeOffIcon from "../../fundamentals/icons/eye-off-icon"
import MedusaPriceInput from "../../organisms/medusa-price-input" import MedusaPriceInput from "../../organisms/medusa-price-input"
const PriceAmount = ({ value, onChange }) => { const PriceAmount = ({ value, onChange }) => {
const { t } = useTranslation()
const { state: showRegions, toggle } = useToggleState() const { state: showRegions, toggle } = useToggleState()
const currencyName = currencies[value.currency_code?.toUpperCase()]?.name const currencyName = currencies[value.currency_code?.toUpperCase()]?.name
@@ -28,7 +30,7 @@ const PriceAmount = ({ value, onChange }) => {
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{showRegions ? <EyeOffIcon size={20} /> : <EyeIcon size={20} />} {showRegions ? <EyeOffIcon size={20} /> : <EyeIcon size={20} />}
<span>Show regions</span> <span>{t("price-overrides-show-regions", "Show regions")}</span>
</div> </div>
</Button> </Button>
) : null} ) : null}
@@ -4,6 +4,7 @@ import qs from "qs"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { useLocation } from "react-router-dom" import { useLocation } from "react-router-dom"
import { usePagination, useTable } from "react-table" import { usePagination, useTable } from "react-table"
import { useTranslation } from "react-i18next"
import ProductsFilter from "../../../domain/products/filter-dropdown" import ProductsFilter from "../../../domain/products/filter-dropdown"
import { useAnalytics } from "../../../providers/analytics-provider" import { useAnalytics } from "../../../providers/analytics-provider"
import { useFeatureFlag } from "../../../providers/feature-flag-provider" import { useFeatureFlag } from "../../../providers/feature-flag-provider"
@@ -26,6 +27,7 @@ const defaultQueryProps = {
const ProductTable = () => { const ProductTable = () => {
const location = useLocation() const location = useLocation()
const { t } = useTranslation()
const { isFeatureEnabled } = useFeatureFlag() const { isFeatureEnabled } = useFeatureFlag()
const { trackNumberOfProducts } = useAnalytics() const { trackNumberOfProducts } = useAnalytics()
@@ -185,7 +187,7 @@ const ProductTable = () => {
count: count!, count: count!,
offset: offs, offset: offs,
pageSize: offs + rows.length, pageSize: offs + rows.length,
title: "Products", title: t("product-table-products", "Products"),
currentPage: pageIndex + 1, currentPage: pageIndex + 1,
pageCount: pageCount, pageCount: pageCount,
nextPage: handleNext, nextPage: handleNext,
@@ -2,6 +2,7 @@ import { AdminPostProductsReq, Product } from "@medusajs/medusa"
import { omit } from "lodash" import { omit } from "lodash"
import { useAdminCreateProduct } from "medusa-react" import { useAdminCreateProduct } from "medusa-react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import useNotification from "../../../hooks/use-notification" import useNotification from "../../../hooks/use-notification"
import { ProductStatus } from "../../../types/shared" import { ProductStatus } from "../../../types/shared"
import { getErrorMessage } from "../../../utils/error-messages" import { getErrorMessage } from "../../../utils/error-messages"
@@ -37,6 +38,7 @@ const useCopyProduct = () => {
const navigate = useNavigate() const navigate = useNavigate()
const notification = useNotification() const notification = useNotification()
const { mutate } = useAdminCreateProduct() const { mutate } = useAdminCreateProduct()
const { t } = useTranslation()
const handleCopyProduct = (product: Product) => { const handleCopyProduct = (product: Product) => {
const { const {
@@ -103,7 +105,7 @@ const useCopyProduct = () => {
"product", "product",
"product_id", "product_id",
"variant_rank", "variant_rank",
"purchasable" "purchasable",
]) ])
const variantBase = Object.entries(rest).reduce((acc, [key, value]) => { const variantBase = Object.entries(rest).reduce((acc, [key, value]) => {
@@ -181,10 +183,21 @@ const useCopyProduct = () => {
mutate(base as AdminPostProductsReq, { mutate(base as AdminPostProductsReq, {
onSuccess: ({ product: copiedProduct }) => { onSuccess: ({ product: copiedProduct }) => {
navigate(`/a/products/${copiedProduct.id}`) navigate(`/a/products/${copiedProduct.id}`)
notification("Success", "Created a new product", "success") notification(
t("product-table-copy-success", "Success"),
t(
"product-table-copy-created-a-new-product",
"Created a new product"
),
"success"
)
}, },
onError: (error) => { onError: (error) => {
notification("Error", getErrorMessage(error), "error") notification(
t("product-table-copy-error", "Error"),
getErrorMessage(error),
"error"
)
}, },
}) })
} }
Loaded 100 of 347 files, more files were not shown because too many files have changed in this diff. Show more