refactor(dashboard): refresh domains (#7087)

This commit is contained in:
Frane Polić authored and GitHub committed 2024-04-24 10:38:52 +02:00
1 parent 40686ba980
commit fe68b5c0f2
49 files changed
+624 -228

No files matched your search

@@ -280,7 +280,12 @@
"manageInventoryLabel": "Manage inventory", "manageInventoryLabel": "Manage inventory",
"manageInventoryHint": "When enabled the inventory level will be regulated when orders and returns are created.", "manageInventoryHint": "When enabled the inventory level will be regulated when orders and returns are created.",
"allowBackordersLabel": "Allow backorders", "allowBackordersLabel": "Allow backorders",
"allowBackordersHint": "When enabled the variant can be sold even if the inventory level is below zero." "allowBackordersHint": "When enabled the variant can be sold even if the inventory level is below zero.",
"toast": {
"levelsBatch": "Inventory levels updated.",
"update": "Inventory item updated successfully",
"updateLevel": "Inventory level updated successfully"
}
} }
}, },
"options": { "options": {
@@ -1018,6 +1023,9 @@
"languageHint": "The language you want to use in the admin dashboard. This will not change the language of your store.", "languageHint": "The language you want to use in the admin dashboard. This will not change the language of your store.",
"languagePlaceholder": "Select language", "languagePlaceholder": "Select language",
"usageInsightsHint": "Share usage insights and help us improve Medusa. You can read more about what we collect and how we use it in our <0>documentation</0>." "usageInsightsHint": "Share usage insights and help us improve Medusa. You can read more about what we collect and how we use it in our <0>documentation</0>."
},
"toast": {
"edit": "Profiles changes saved"
} }
}, },
"users": { "users": {
@@ -1051,6 +1059,7 @@
"manageYourStoresDetails": "Manage your store's details", "manageYourStoresDetails": "Manage your store's details",
"editStore": "Edit store", "editStore": "Edit store",
"defaultCurrency": "Default currency", "defaultCurrency": "Default currency",
"defaultRegion": "Default region",
"swapLinkTemplate": "Swap link template", "swapLinkTemplate": "Swap link template",
"paymentLinkTemplate": "Payment link template", "paymentLinkTemplate": "Payment link template",
"inviteLinkTemplate": "Invite link template", "inviteLinkTemplate": "Invite link template",
@@ -1061,6 +1070,11 @@
"currencyAlreadyAdded": "The currency has already been added to your store.", "currencyAlreadyAdded": "The currency has already been added to your store.",
"edit": { "edit": {
"header": "Edit Store" "header": "Edit Store"
},
"toast": {
"update": "Store successfully updated",
"currenciesUpdated": "Currencies updated successfully",
"currenciesRemoved": "Removed currencies from the store successfully"
} }
}, },
"regions": { "regions": {
@@ -1171,7 +1185,12 @@
"selectLocations": "Select locations that stock the item.", "selectLocations": "Select locations that stock the item.",
"deleteLocationWarning": "You are about to delete the location {{name}}. This action cannot be undone.", "deleteLocationWarning": "You are about to delete the location {{name}}. This action cannot be undone.",
"removeSalesChannelsWarning_one": "You are about to remove {{count}} sales channel from the location.", "removeSalesChannelsWarning_one": "You are about to remove {{count}} sales channel from the location.",
"removeSalesChannelsWarning_other": "You are about to remove {{count}} sales channels from the location." "removeSalesChannelsWarning_other": "You are about to remove {{count}} sales channels from the location.",
"toast": {
"create": "Location created sucessfully",
"update": "Location updated sucessfully",
"removeChannel": "Sales channel removed sucessfully"
}
}, },
"reservations": { "reservations": {
"domain": "Reservations", "domain": "Reservations",
@@ -1187,7 +1206,12 @@
"addProducts": "Add Products", "addProducts": "Add Products",
"editSalesChannel": "Edit sales channel", "editSalesChannel": "Edit sales channel",
"productAlreadyAdded": "The product has already been added to the sales channel.", "productAlreadyAdded": "The product has already been added to the sales channel.",
"deleteSalesChannelWarning": "You are about to delete the sales channel {{name}}. This action cannot be undone." "deleteSalesChannelWarning": "You are about to delete the sales channel {{name}}. This action cannot be undone.",
"toast": {
"create": "Sales channel created successfully",
"update": "Sales channel updated successfully",
"delete": "Sales channel deleted successfully"
}
}, },
"apiKeyManagement": { "apiKeyManagement": {
"domain": { "domain": {
@@ -1279,7 +1303,10 @@
"successAction": "Sign in to start using Medusa", "successAction": "Sign in to start using Medusa",
"invalidTokenTitle": "Your invite token is invalid", "invalidTokenTitle": "Your invite token is invalid",
"invalidTokenHint": "Try requesting a new invite link.", "invalidTokenHint": "Try requesting a new invite link.",
"passwordMismatch": "Passwords do not match" "passwordMismatch": "Passwords do not match",
"toast": {
"accepted": "Invite successfully accepted"
}
}, },
"resetPassword": { "resetPassword": {
"title": "Reset password", "title": "Reset password",
@@ -167,6 +167,7 @@ export type InventoryItemRes = {
stocked_quantity: number stocked_quantity: number
reserved_quantity: number reserved_quantity: number
location_levels?: InventoryNext.InventoryLevelDTO[] location_levels?: InventoryNext.InventoryLevelDTO[]
variant?: ProductVariantDTO | ProductVariantDTO[]
} }
} }
@@ -13,7 +13,7 @@ export const AdjustInventoryDrawer = () => {
const { const {
inventory_item: inventoryItem, inventory_item: inventoryItem,
isLoading, isPending: isLoading,
isError, isError,
error, error,
} = useInventoryItem(id!) } = useInventoryItem(id!)
@@ -1,6 +1,6 @@
import * as zod from "zod" import * as zod from "zod"
import { Button, Input, Text } from "@medusajs/ui" import { Button, Input, Text, toast } from "@medusajs/ui"
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/types" import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/types"
import { import {
RouteDrawer, RouteDrawer,
@@ -61,7 +61,7 @@ export const AdjustInventoryForm = ({
const stockedQuantityUpdate = form.watch("stocked_quantity") const stockedQuantityUpdate = form.watch("stocked_quantity")
const { mutateAsync } = useUpdateInventoryItemLevel( const { mutateAsync, isPending: isLoading } = useUpdateInventoryItemLevel(
item.id, item.id,
level.location_id level.location_id
) )
@@ -71,9 +71,21 @@ export const AdjustInventoryForm = ({
return handleSuccess() return handleSuccess()
} }
await mutateAsync({ try {
stocked_quantity: value.stocked_quantity, await mutateAsync({
}) stocked_quantity: value.stocked_quantity,
})
toast.success(t("general.success"), {
description: t("inventory.toast.updateLevel"),
dismissLabel: t("actions.close"),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
return handleSuccess() return handleSuccess()
}) })
@@ -141,7 +153,7 @@ export const AdjustInventoryForm = ({
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</RouteDrawer.Close> </RouteDrawer.Close>
<Button type="submit" size="small" isLoading={false}> <Button type="submit" size="small" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
@@ -1,6 +1,6 @@
import * as zod from "zod" import * as zod from "zod"
import { Button, Input } from "@medusajs/ui" import { Button, Input, toast } from "@medusajs/ui"
import { import {
RouteDrawer, RouteDrawer,
useRouteModal, useRouteModal,
@@ -52,13 +52,22 @@ export const EditInventoryItemAttributesForm = ({
resolver: zodResolver(EditInventoryItemAttributesSchema), resolver: zodResolver(EditInventoryItemAttributesSchema),
}) })
const { mutateAsync } = useUpdateInventoryItem(item.id) const { mutateAsync, isPending: isLoading } = useUpdateInventoryItem(item.id)
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
mutateAsync(values, { await mutateAsync(values, {
onSuccess: () => { onSuccess: () => {
toast.success(t("general.success"), {
description: t("inventory.toast.update"),
dismissLabel: t("actions.close"),
})
handleSuccess() handleSuccess()
}, },
onError: (error) =>
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
}),
}) })
}) })
@@ -241,7 +250,7 @@ export const EditInventoryItemAttributesForm = ({
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</RouteDrawer.Close> </RouteDrawer.Close>
<Button type="submit" size="small" isLoading={false}> <Button type="submit" size="small" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
@@ -11,7 +11,7 @@ export const InventoryItemAttributesEdit = () => {
const { const {
inventory_item: inventoryItem, inventory_item: inventoryItem,
isLoading, isPending: isLoading,
isError, isError,
error, error,
} = useInventoryItem(id!) } = useInventoryItem(id!)
@@ -1,6 +1,6 @@
import * as zod from "zod" import * as zod from "zod"
import { Button, Input } from "@medusajs/ui" import { Button, Input, toast } from "@medusajs/ui"
import { import {
RouteDrawer, RouteDrawer,
useRouteModal, useRouteModal,
@@ -39,13 +39,22 @@ export const EditInventoryItemForm = ({ item }: EditInventoryItemFormProps) => {
resolver: zodResolver(EditInventoryItemSchema), resolver: zodResolver(EditInventoryItemSchema),
}) })
const { mutateAsync } = useUpdateInventoryItem(item.id) const { mutateAsync, isPending: isLoading } = useUpdateInventoryItem(item.id)
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
mutateAsync(values as any, { mutateAsync(values as any, {
onSuccess: () => { onSuccess: () => {
toast.success(t("general.success"), {
description: t("inventory.toast.update"),
dismissLabel: t("actions.close"),
})
handleSuccess() handleSuccess()
}, },
onError: (e) =>
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
}),
}) })
}) })
@@ -94,7 +103,7 @@ export const EditInventoryItemForm = ({ item }: EditInventoryItemFormProps) => {
{t("actions.cancel")} {t("actions.cancel")}
</Button> </Button>
</RouteDrawer.Close> </RouteDrawer.Close>
<Button type="submit" size="small" isLoading={false}> <Button type="submit" size="small" isLoading={isLoading}>
{t("actions.save")} {t("actions.save")}
</Button> </Button>
</div> </div>
@@ -11,7 +11,7 @@ export const InventoryItemEdit = () => {
const { const {
inventory_item: inventoryItem, inventory_item: inventoryItem,
isLoading, isPending: isLoading,
isError, isError,
error, error,
} = useInventoryItem(id!) } = useInventoryItem(id!)
@@ -1,5 +1,5 @@
import { Container, Heading, Text } from "@medusajs/ui" import { Container, Heading } from "@medusajs/ui"
import { InventoryNext, ProductVariantDTO } from "@medusajs/types" import { ProductVariantDTO } from "@medusajs/types"
import { ActionMenu } from "../../../../components/common/action-menu" import { ActionMenu } from "../../../../components/common/action-menu"
import { InventoryItemRes } from "../../../../types/api-responses" import { InventoryItemRes } from "../../../../types/api-responses"
@@ -52,7 +52,7 @@ export const InventoryItemGeneralSection = ({
title={t("fields.inStock")} title={t("fields.inStock")}
value={getQuantityFormat( value={getQuantityFormat(
inventoryItem.stocked_quantity, inventoryItem.stocked_quantity,
inventoryItem.location_levels.length inventoryItem.location_levels?.length
)} )}
/> />
@@ -60,14 +60,14 @@ export const InventoryItemGeneralSection = ({
title={t("inventory.reserved")} title={t("inventory.reserved")}
value={getQuantityFormat( value={getQuantityFormat(
inventoryItem.reserved_quantity, inventoryItem.reserved_quantity,
inventoryItem.location_levels.length inventoryItem.location_levels?.length
)} )}
/> />
<SectionRow <SectionRow
title={t("inventory.available")} title={t("inventory.available")}
value={getQuantityFormat( value={getQuantityFormat(
inventoryItem.stocked_quantity - inventoryItem.reserved_quantity, inventoryItem.stocked_quantity - inventoryItem.reserved_quantity,
inventoryItem.location_levels.length inventoryItem.location_levels?.length
)} )}
/> />
</Container> </Container>
@@ -3,7 +3,6 @@ import { Button, Container, Heading } from "@medusajs/ui"
import { InventoryItemRes } from "../../../../types/api-responses" import { InventoryItemRes } from "../../../../types/api-responses"
import { ItemLocationListTable } from "./location-levels-table/location-list-table" import { ItemLocationListTable } from "./location-levels-table/location-list-table"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { ReservationItemTable } from "./reservations-table/reservation-list-table"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
type InventoryItemLocationLevelsSectionProps = { type InventoryItemLocationLevelsSectionProps = {
@@ -15,11 +15,16 @@ export const ItemLocationListTable = ({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
}) })
const { inventory_levels, count, isLoading, isError, error } = const {
useInventoryItemLevels(inventory_item_id, { inventory_levels,
...searchParams, count,
fields: "*stock_locations", isPending: isLoading,
}) isError,
error,
} = useInventoryItemLevels(inventory_item_id, {
...searchParams,
fields: "*stock_locations",
})
const columns = useLocationListTableColumns() const columns = useLocationListTableColumns()
@@ -15,7 +15,7 @@ export const LocationItem = ({
}: LocationItemProps) => { }: LocationItemProps) => {
return ( return (
<div <div
className={clx("flex w-full rounded-lg border px-2 py-2 gap-x-2", { className={clx("flex w-full gap-x-2 rounded-lg border px-2 py-2", {
"border-ui-border-interactive ": selected, "border-ui-border-interactive ": selected,
})} })}
onClick={() => onSelect(!selected)} onClick={() => onSelect(!selected)}
@@ -1,14 +1,11 @@
import * as zod from "zod" import * as zod from "zod"
import { Button, Table, Text } from "@medusajs/ui" import { Button, Text, toast } from "@medusajs/ui"
import { import {
RouteDrawer, RouteDrawer,
useRouteModal, useRouteModal,
} from "../../../../../../components/route-modal" } from "../../../../../../components/route-modal"
import { import { useBatchInventoryItemLevels } from "../../../../../../hooks/api/inventory"
useBatchInventoryItemLevels,
useUpdateInventoryItem,
} from "../../../../../../hooks/api/inventory"
import { useFieldArray, useForm } from "react-hook-form" import { useFieldArray, useForm } from "react-hook-form"
import { InventoryItemRes } from "../../../../../../types/api-responses" import { InventoryItemRes } from "../../../../../../types/api-responses"
@@ -35,13 +32,13 @@ const EditInventoryItemAttributesSchema = z.object({
const getDefaultValues = ( const getDefaultValues = (
allLocations: StockLocationDTO[], allLocations: StockLocationDTO[],
existinLevels: Set<string> existingLevels: Set<string>
) => { ) => {
return { return {
locations: allLocations.map((location) => ({ locations: allLocations.map((location) => ({
...location, ...location,
location_id: location.id, location_id: location.id,
selected: existinLevels.has(location.id), selected: existingLevels.has(location.id),
})), })),
} }
} }
@@ -97,14 +94,26 @@ export const ManageLocationsForm = ({
return handleSuccess() return handleSuccess()
} }
await mutateAsync({ try {
creates: selectedLocations.map((location_id) => ({ await mutateAsync({
location_id, creates: selectedLocations.map((location_id) => ({
})), location_id,
deletes: unselectedLocations, })),
}) deletes: unselectedLocations,
})
return handleSuccess() handleSuccess()
toast.success(t("general.success"), {
description: t("inventory.toast.update"),
dismissLabel: t("actions.close"),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
}) })
return ( return (
@@ -114,7 +123,7 @@ export const ManageLocationsForm = ({
className="flex flex-1 flex-col overflow-hidden" className="flex flex-1 flex-col overflow-hidden"
> >
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-4 overflow-auto"> <RouteDrawer.Body className="flex flex-1 flex-col gap-y-4 overflow-auto">
<div className="grid grid-rows-2 divide-y rounded-lg border text-ui-fg-subtle shadow-elevation-card-rest"> <div className="text-ui-fg-subtle shadow-elevation-card-rest grid grid-rows-2 divide-y rounded-lg border">
<div className="grid grid-cols-2 divide-x"> <div className="grid grid-cols-2 divide-x">
<Text className="px-2 py-1.5" size="small" leading="compact"> <Text className="px-2 py-1.5" size="small" leading="compact">
{t("fields.title")} {t("fields.title")}
@@ -136,7 +145,7 @@ export const ManageLocationsForm = ({
<Text size="small" weight="plus" leading="compact"> <Text size="small" weight="plus" leading="compact">
{t("locations.domain")} {t("locations.domain")}
</Text> </Text>
<div className="flex w-full justify-between text-ui-fg-subtle"> <div className="text-ui-fg-subtle flex w-full justify-between">
<Text size="small" leading="compact"> <Text size="small" leading="compact">
{t("locations.selectLocations")} {t("locations.selectLocations")}
</Text> </Text>
@@ -12,7 +12,7 @@ export const ManageLocationsDrawer = () => {
const { const {
inventory_item: inventoryItem, inventory_item: inventoryItem,
isLoading, isPending: isLoading,
isError, isError,
error, error,
} = useInventoryItem(id!) } = useInventoryItem(id!)
@@ -16,11 +16,16 @@ export const ReservationItemTable = ({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
}) })
const { reservations, count, isLoading, isError, error } = const {
useReservationItems({ reservations,
...searchParams, count,
inventory_item_id: [inventoryItem.id], isPending: isLoading,
}) isError,
error,
} = useReservationItems({
...searchParams,
inventory_item_id: [inventoryItem.id],
})
const columns = useInventoryTableColumns({ sku: inventoryItem.sku! }) const columns = useInventoryTableColumns({ sku: inventoryItem.sku! })
@@ -1,2 +1,2 @@
export { InventoryDeatil as Component } from "./inventory-detail" export { InventoryDetail as Component } from "./inventory-detail"
export { inventoryItemLoader as loader } from "./loader" export { inventoryItemLoader as loader } from "./loader"
@@ -5,19 +5,26 @@ import { InventoryItemGeneralSection } from "./components/inventory-item-general
import { InventoryItemLocationLevelsSection } from "./components/inventory-item-location-levels" import { InventoryItemLocationLevelsSection } from "./components/inventory-item-location-levels"
import { InventoryItemReservationsSection } from "./components/inventory-item-reservations" import { InventoryItemReservationsSection } from "./components/inventory-item-reservations"
import { JsonViewSection } from "../../../components/common/json-view-section" import { JsonViewSection } from "../../../components/common/json-view-section"
import { inventoryItemLoader } from "./loader"
import { useInventoryItem } from "../../../hooks/api/inventory" import { useInventoryItem } from "../../../hooks/api/inventory"
import { inventoryItemLoader } from "./loader"
export const InventoryDeatil = () => { export const InventoryDetail = () => {
const { id } = useParams() const { id } = useParams()
const initialData = useLoaderData() as Awaited< const initialData = useLoaderData() as Awaited<
ReturnType<typeof inventoryItemLoader> ReturnType<typeof inventoryItemLoader>
> >
const { inventory_item, isLoading, isError, error } = useInventoryItem( const {
inventory_item,
isPending: isLoading,
isError,
error,
} = useInventoryItem(
id!, id!,
{}, {
fields: "*variants",
},
{ {
initialData, initialData,
} }
@@ -38,7 +45,7 @@ export const InventoryDeatil = () => {
return ( return (
<div className="flex flex-col gap-y-2"> <div className="flex flex-col gap-y-2">
<div className="flex flex-col gap-x-4 lg:flex-row lg:items-start"> <div className="flex flex-col gap-x-4 lg:flex-row lg:items-start">
<div className="w-full flex flex-col gap-y-2"> <div className="flex w-full flex-col gap-y-2">
<InventoryItemGeneralSection inventoryItem={inventory_item} /> <InventoryItemGeneralSection inventoryItem={inventory_item} />
<InventoryItemLocationLevelsSection inventoryItem={inventory_item} /> <InventoryItemLocationLevelsSection inventoryItem={inventory_item} />
<InventoryItemReservationsSection inventoryItem={inventory_item} /> <InventoryItemReservationsSection inventoryItem={inventory_item} />
@@ -47,7 +54,7 @@ export const InventoryDeatil = () => {
</div> </div>
<Outlet /> <Outlet />
</div> </div>
<div className="w-full lg:max-w-[400px] max-w-[100%] mt-2 lg:mt-0 flex flex-col gap-y-2"> <div className="mt-2 flex w-full max-w-[100%] flex-col gap-y-2 lg:mt-0 lg:max-w-[400px]">
<InventoryItemAttributeSection inventoryItem={inventory_item} /> <InventoryItemAttributeSection inventoryItem={inventory_item} />
<div className="lg:hidden"> <div className="lg:hidden">
<JsonViewSection data={inventory_item} /> <JsonViewSection data={inventory_item} />
@@ -1,7 +1,7 @@
import { Container, Heading } from "@medusajs/ui" import { Container, Heading } from "@medusajs/ui"
import { InventoryNext } from "@medusajs/types"
import { DataTable } from "../../../../components/table/data-table" import { DataTable } from "../../../../components/table/data-table"
import { InventoryNext } from "@medusajs/types"
import { useDataTable } from "../../../../hooks/use-data-table" import { useDataTable } from "../../../../hooks/use-data-table"
import { useInventoryItems } from "../../../../hooks/api/inventory" import { useInventoryItems } from "../../../../hooks/api/inventory"
import { useInventoryTableColumns } from "./use-inventory-table-columns" import { useInventoryTableColumns } from "./use-inventory-table-columns"
@@ -17,10 +17,16 @@ export const InventoryListTable = () => {
const { searchParams, raw } = useInventoryTableQuery({ const { searchParams, raw } = useInventoryTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
}) })
const { inventory_items, count, isLoading, isError, error } =
useInventoryItems({ const {
...searchParams, inventory_items,
}) count,
isPending: isLoading,
isError,
error,
} = useInventoryItems({
...searchParams,
})
const filters = useInventoryTableFilters() const filters = useInventoryTableFilters()
const columns = useInventoryTableColumns() const columns = useInventoryTableColumns()
@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { UserRoles } from "@medusajs/medusa" import { UserRoles } from "@medusajs/medusa"
import { Alert, Button, Heading, Input, Text } from "@medusajs/ui" import { Alert, Button, Heading, Input, Text, toast } from "@medusajs/ui"
import { AnimatePresence, motion } from "framer-motion" import { AnimatePresence, motion } from "framer-motion"
import { Trans, useTranslation } from "react-i18next" import { Trans, useTranslation } from "react-i18next"
import { Link, useSearchParams } from "react-router-dom" import { Link, useSearchParams } from "react-router-dom"
@@ -49,7 +49,7 @@ export const Invite = () => {
const isValidInvite = invite && validateDecodedInvite(invite) const isValidInvite = invite && validateDecodedInvite(invite)
return ( return (
<div className="min-h-dvh w-dvw bg-ui-bg-base relative flex items-center justify-center p-4"> <div className="bg-ui-bg-base relative flex min-h-dvh w-dvw items-center justify-center p-4">
<div className="flex w-full max-w-[300px] flex-col items-center"> <div className="flex w-full max-w-[300px] flex-col items-center">
<LogoBox <LogoBox
className="mb-4" className="mb-4"
@@ -204,10 +204,10 @@ const CreateView = ({
}, },
}) })
const { mutateAsync: createAuthUser, isLoading: isCreatingAuthUser } = const { mutateAsync: createAuthUser, isPending: isCreatingAuthUser } =
useV2CreateAuthUser() useV2CreateAuthUser()
const { mutateAsync: acceptInvite, isLoading: isAcceptingInvite } = const { mutateAsync: acceptInvite, isPending: isAcceptingInvite } =
useV2AcceptInvite(token) useV2AcceptInvite(token)
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit(async (data) => {
@@ -228,6 +228,10 @@ const CreateView = ({
token: authToken, token: authToken,
}) })
onSuccess() onSuccess()
toast.success(t("general.success"), {
description: t("invite.toast.accepted"),
dismissLabel: t("actions.close"),
})
} catch (error) { } catch (error) {
if (isAxiosError(error) && error.response?.status === 400) { if (isAxiosError(error) && error.response?.status === 400) {
form.setError("root", { form.setError("root", {
@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Heading, Input, Text } from "@medusajs/ui" import { Button, Heading, Input, Text, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
@@ -49,17 +49,24 @@ export const CreateLocationForm = () => {
const { mutateAsync, isPending } = useCreateStockLocation() const { mutateAsync, isPending } = useCreateStockLocation()
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
mutateAsync( try {
{ await mutateAsync({
name: values.name, name: values.name,
address: values.address, address: values.address,
}, })
{
onSuccess: () => { handleSuccess("/settings/locations")
handleSuccess("/settings/locations")
}, toast.success(t("general.success"), {
} description: t("locations.toast.create"),
) dismissLabel: t("actions.close"),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
}) })
return ( return (
@@ -1,6 +1,6 @@
import { PencilSquare, Trash } from "@medusajs/icons" import { PencilSquare, Trash } from "@medusajs/icons"
import { SalesChannelDTO } from "@medusajs/types" import { SalesChannelDTO } from "@medusajs/types"
import { Button, Container, Heading, usePrompt } from "@medusajs/ui" import { Button, Container, Heading, toast, usePrompt } from "@medusajs/ui"
import { createColumnHelper } from "@tanstack/react-table" import { createColumnHelper } from "@tanstack/react-table"
import { useAdminRemoveLocationFromSalesChannel } from "medusa-react" import { useAdminRemoveLocationFromSalesChannel } from "medusa-react"
import { useMemo } from "react" import { useMemo } from "react"
@@ -82,10 +82,22 @@ const SalesChannelActions = ({
return return
} }
await mutateAsync({ try {
location_id: locationId, await mutateAsync({
sales_channel_id: salesChannel.id, location_id: locationId,
}) sales_channel_id: salesChannel.id,
})
toast.success(t("general.success"), {
description: t("locations.toast.removeChannel"),
dismissLabel: t("actions.close"),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
} }
return ( return (
@@ -7,7 +7,12 @@ import { LocationSalesChannelSection } from "./components/location-sales-channel
export const LocationDetail = () => { export const LocationDetail = () => {
const { id } = useParams() const { id } = useParams()
const { stock_location, isLoading, isError, error } = useStockLocation(id!, { const {
stock_location,
isPending: isLoading,
isError,
error,
} = useStockLocation(id!, {
fields: "*address,*sales_channels", fields: "*address,*sales_channels",
}) })
@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Input } from "@medusajs/ui" import { Button, Input, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
@@ -55,17 +55,23 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
const { mutateAsync, isPending } = useUpdateStockLocation(location.id) const { mutateAsync, isPending } = useUpdateStockLocation(location.id)
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
mutateAsync( try {
{ await mutateAsync({
name: values.name, name: values.name,
address: values.address, address: values.address,
}, })
{ handleSuccess()
onSuccess: () => {
handleSuccess() toast.success(t("general.success"), {
}, description: t("locations.toast.update"),
} dismissLabel: t("actions.close"),
) })
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
}) })
return ( return (
@@ -2,14 +2,18 @@ import { Heading } from "@medusajs/ui"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { RouteDrawer } from "../../../components/route-modal" import { RouteDrawer } from "../../../components/route-modal"
import { useStockLocations } from "../../../hooks/api/stock-locations" import { useStockLocation } from "../../../hooks/api/stock-locations"
import { EditLocationForm } from "./components/edit-location-form" import { EditLocationForm } from "./components/edit-location-form"
export const LocationEdit = () => { export const LocationEdit = () => {
const { id } = useParams() const { id } = useParams()
const { stock_locations, isLoading, isError, error } = useStockLocations({ const {
id, stock_location,
isPending: isLoading,
isError,
error,
} = useStockLocation(id, {
fields: "*address", fields: "*address",
}) })
@@ -19,8 +23,6 @@ export const LocationEdit = () => {
throw error throw error
} }
const stock_location = stock_locations?.[0]
return ( return (
<RouteDrawer> <RouteDrawer>
<RouteDrawer.Header> <RouteDrawer.Header>
@@ -19,16 +19,21 @@ export const LocationsListTable = () => {
* Note: The endpoint is bugged and does not return count, causing the table to not render * Note: The endpoint is bugged and does not return count, causing the table to not render
* any rows. * any rows.
*/ */
const { stock_locations, count, isLoading, isError, error } = const {
useStockLocations({ stock_locations = [],
...searchParams, count,
fields: "*address", isLoading,
}) isError,
error,
} = useStockLocations({
...searchParams,
fields: "*address",
})
const columns = useLocationTableColumns() const columns = useLocationTableColumns()
const { table } = useDataTable({ const { table } = useDataTable({
data: stock_locations ?? [], data: stock_locations,
columns, columns,
count, count,
enablePagination: true, enablePagination: true,
@@ -56,7 +61,8 @@ export const LocationsListTable = () => {
count={count || 1} count={count || 1}
columns={columns} columns={columns}
navigateTo={(row) => row.id} navigateTo={(row) => row.id}
isLoading={isLoading} // TODO: revisit loader - on query change this will cause unmounting of the table, rendering loader briefly and again rendering table which will make search input unfocused
// isLoading={isLoading}
queryObject={raw} queryObject={raw}
pagination pagination
search search
@@ -7,11 +7,12 @@ export const useLocationTableQuery = ({
pageSize?: number pageSize?: number
prefix?: string prefix?: string
}) => { }) => {
const raw = useQueryParams(["offset"], prefix) const raw = useQueryParams(["q", "offset"], prefix)
const searchParams = { const searchParams = {
limit: pageSize, limit: pageSize,
offset: raw.offset, offset: raw.offset,
q: raw.q,
} }
return { return {
@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Heading, Input, Text } from "@medusajs/ui" import { Alert, Button, Heading, Input, Text } from "@medusajs/ui"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next" import { Trans, useTranslation } from "react-i18next"
import { Link, useLocation, useNavigate } from "react-router-dom" import { Link, useLocation, useNavigate } from "react-router-dom"
@@ -34,40 +34,38 @@ export const Login = () => {
const { mutateAsync, isPending } = useEmailPassLogin() const { mutateAsync, isPending } = useEmailPassLogin()
const handleSubmit = form.handleSubmit(async ({ email, password }) => { const handleSubmit = form.handleSubmit(async ({ email, password }) => {
await mutateAsync( try {
{ await mutateAsync({
email, email,
password, password,
}, })
{
onSuccess: () => {
navigate(from, { replace: true })
},
onError: (error) => {
if (isAxiosError(error)) {
if (error.response?.status === 401) {
form.setError("email", {
type: "manual",
})
form.setError("password", { navigate(from, { replace: true })
type: "manual", } catch (error) {
message: t("errors.invalidCredentials"), if (isAxiosError(error)) {
}) if (error.response?.status === 401) {
form.setError("email", {
return
}
}
form.setError("root.serverError", {
type: "manual", type: "manual",
message: t("errors.serverError"),
}) })
},
form.setError("password", {
type: "manual",
message: t("errors.invalidCredentials"),
})
return
}
} }
)
form.setError("root.serverError", {
type: "manual",
message: t("errors.serverError"),
})
}
}) })
const serverError = form.formState.errors?.root?.serverError?.message
return ( return (
<div className="bg-ui-bg-base flex min-h-dvh w-dvw items-center justify-center"> <div className="bg-ui-bg-base flex min-h-dvh w-dvw items-center justify-center">
<div className="m-4 flex w-full max-w-[300px] flex-col items-center"> <div className="m-4 flex w-full max-w-[300px] flex-col items-center">
@@ -123,6 +121,11 @@ export const Login = () => {
{t("actions.continue")} {t("actions.continue")}
</Button> </Button>
</form> </form>
{serverError && (
<Alert className="mt-4" dismissible variant="error">
{serverError}
</Alert>
)}
</Form> </Form>
<div className="my-6 h-px w-full border-b border-dotted" /> <div className="my-6 h-px w-full border-b border-dotted" />
<span className="text-ui-fg-subtle txt-small"> <span className="text-ui-fg-subtle txt-small">
@@ -3,7 +3,7 @@ import { useMe } from "../../../hooks/api/users"
import { ProfileGeneralSection } from "./components/profile-general-section" import { ProfileGeneralSection } from "./components/profile-general-section"
export const ProfileDetail = () => { export const ProfileDetail = () => {
const { user, isLoading, isError, error } = useMe() const { user, isPending: isLoading, isError, error } = useMe()
if (isLoading) { if (isLoading) {
return <div>Loading...</div> return <div>Loading...</div>
@@ -14,7 +14,7 @@ export const ProfileDetail = () => {
throw error throw error
} }
throw json("An unknown error has occured", 500) throw json("An unknown error has occurred", 500)
} }
return ( return (
@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Input, Select, Switch } from "@medusajs/ui" import { Button, Input, Select, Switch, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next" import { Trans, useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
@@ -39,8 +39,8 @@ export const EditProfileForm = ({ user, usageInsights }: EditProfileProps) => {
resolver: zodResolver(EditProfileSchema), resolver: zodResolver(EditProfileSchema),
}) })
const changeLanguage = (code: string) => { const changeLanguage = async (code: string) => {
i18n.changeLanguage(code) await i18n.changeLanguage(code)
} }
const sortedLanguages = languages.sort((a, b) => const sortedLanguages = languages.sort((a, b) =>
@@ -50,21 +50,26 @@ export const EditProfileForm = ({ user, usageInsights }: EditProfileProps) => {
const { mutateAsync, isPending } = useUpdateUser(user.id!) const { mutateAsync, isPending } = useUpdateUser(user.id!)
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync( try {
{ await mutateAsync({
first_name: values.first_name, first_name: values.first_name,
last_name: values.last_name, last_name: values.last_name,
}, })
{
onError: () => {
return
},
}
)
changeLanguage(values.language) await changeLanguage(values.language)
handleSuccess() handleSuccess()
toast.success(t("general.success"), {
description: t("profile.toast.edit"),
dismissLabel: t("actions.close"),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
}) })
return ( return (
@@ -5,7 +5,7 @@ import { useMe } from "../../../hooks/api/users"
import { EditProfileForm } from "./components/edit-profile-form/edit-profile-form" import { EditProfileForm } from "./components/edit-profile-form/edit-profile-form"
export const ProfileEdit = () => { export const ProfileEdit = () => {
const { user, isLoading, isError, error } = useMe() const { user, isPending: isLoading, isError, error } = useMe()
const { t } = useTranslation() const { t } = useTranslation()
@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { SalesChannelDTO } from "@medusajs/types" import { SalesChannelDTO } from "@medusajs/types"
import { Button, Checkbox, Hint, Tooltip } from "@medusajs/ui" import { Button, Checkbox, Hint, toast, Tooltip } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query" import { keepPreviousData } from "@tanstack/react-query"
import { import {
OnChangeFn, OnChangeFn,
@@ -67,7 +67,13 @@ export const AddProductsToSalesChannelForm = ({
} }
const { searchParams, raw } = useProductTableQuery({ pageSize: PAGE_SIZE }) const { searchParams, raw } = useProductTableQuery({ pageSize: PAGE_SIZE })
const { products, count, isLoading, isError, error } = useProducts( const {
products,
count,
isPending: isLoading,
isError,
error,
} = useProducts(
{ {
expand: "variants,sales_channels", expand: "variants,sales_channels",
...searchParams, ...searchParams,
@@ -108,8 +114,17 @@ export const AddProductsToSalesChannelForm = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success(t("general.success"), {
description: t("salesChannels.toast.update"),
dismissLabel: t("actions.close"),
})
handleSuccess() handleSuccess()
}, },
onError: (error) =>
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
}),
} }
) )
}) })
@@ -5,7 +5,12 @@ import { AddProductsToSalesChannelForm } from "./components"
export const SalesChannelAddProducts = () => { export const SalesChannelAddProducts = () => {
const { id } = useParams() const { id } = useParams()
const { sales_channel, isLoading, isError, error } = useSalesChannel(id!) const {
sales_channel,
isPending: isLoading,
isError,
error,
} = useSalesChannel(id!)
if (isError) { if (isError) {
throw error throw error
@@ -1,5 +1,13 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Heading, Input, Switch, Text, Textarea } from "@medusajs/ui" import {
Button,
Heading,
Input,
Switch,
Text,
Textarea,
toast,
} from "@medusajs/ui"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
@@ -40,8 +48,17 @@ export const CreateSalesChannelForm = () => {
}, },
{ {
onSuccess: ({ sales_channel }) => { onSuccess: ({ sales_channel }) => {
toast.success(t("general.success"), {
description: t("salesChannels.toast.create"),
dismissLabel: t("actions.close"),
})
handleSuccess(`../${sales_channel.id}`) handleSuccess(`../${sales_channel.id}`)
}, },
onError: (error) =>
toast.success(t("general.success"), {
description: error.message,
dismissLabel: t("actions.close"),
}),
} }
) )
}) })
@@ -1,5 +1,12 @@
import { PencilSquare, Trash } from "@medusajs/icons" import { PencilSquare, Trash } from "@medusajs/icons"
import { Container, Heading, StatusBadge, Text, usePrompt } from "@medusajs/ui" import {
Container,
Heading,
StatusBadge,
Text,
toast,
usePrompt,
} from "@medusajs/ui"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { SalesChannelDTO } from "@medusajs/types" import { SalesChannelDTO } from "@medusajs/types"
@@ -36,11 +43,21 @@ export const SalesChannelGeneralSection = ({
return return
} }
await mutateAsync(undefined, { try {
onSuccess: () => { await mutateAsync()
navigate("/settings/sales-channels", { replace: true })
}, navigate("/settings/sales-channels", { replace: true })
})
toast.success(t("general.success"), {
description: t("salesChannels.toast.delete"),
dismissLabel: t("actions.close"),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
} }
return ( return (
@@ -1,5 +1,12 @@
import { PencilSquare, Trash } from "@medusajs/icons" import { PencilSquare, Trash } from "@medusajs/icons"
import { Button, Checkbox, Container, Heading, usePrompt } from "@medusajs/ui" import {
Button,
Checkbox,
Container,
Heading,
toast,
usePrompt,
} from "@medusajs/ui"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table" import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import { useMemo, useState } from "react" import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
@@ -29,7 +36,13 @@ export const SalesChannelProductSection = ({
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const { searchParams, raw } = useProductTableQuery({ pageSize: PAGE_SIZE }) const { searchParams, raw } = useProductTableQuery({ pageSize: PAGE_SIZE })
const { products, count, isLoading, isError, error } = useProducts( const {
products,
count,
isPending: isLoading,
isError,
error,
} = useProducts(
{ {
...searchParams, ...searchParams,
sales_channel_id: [salesChannel.id], sales_channel_id: [salesChannel.id],
@@ -87,8 +100,18 @@ export const SalesChannelProductSection = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success(t("general.success"), {
description: t("salesChannels.toast.update"),
dismissLabel: t("actions.close"),
})
setRowSelection({}) setRowSelection({})
}, },
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
})
},
} }
) )
} }
@@ -199,9 +222,20 @@ const ProductListCellActions = ({
const { mutateAsync } = useSalesChannelRemoveProducts(salesChannelId) const { mutateAsync } = useSalesChannelRemoveProducts(salesChannelId)
const onRemove = async () => { const onRemove = async () => {
await mutateAsync({ try {
product_ids: [productId], await mutateAsync({
}) product_ids: [productId],
})
toast.success(t("general.success"), {
description: t("salesChannels.toast.update"),
dismissLabel: t("actions.close"),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
} }
return ( return (
@@ -12,7 +12,7 @@ export const SalesChannelDetail = () => {
> >
const { id } = useParams() const { id } = useParams()
const { sales_channel, isLoading } = useSalesChannel(id!, { const { sales_channel, isPending: isLoading } = useSalesChannel(id!, {
initialData, initialData,
}) })
@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Input, Switch, Textarea } from "@medusajs/ui" import { Button, Input, Switch, Textarea, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import * as zod from "zod" import * as zod from "zod"
@@ -48,8 +48,18 @@ export const EditSalesChannelForm = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success(t("general.success"), {
description: t("salesChannels.toast.update"),
dismissLabel: t("actions.close"),
})
handleSuccess() handleSuccess()
}, },
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
})
},
} }
) )
}) })
@@ -10,7 +10,12 @@ export const SalesChannelEdit = () => {
const { id } = useParams() const { id } = useParams()
const { t } = useTranslation() const { t } = useTranslation()
const { sales_channel, isLoading, isError, error } = useSalesChannel(id!) const {
sales_channel,
isPending: isLoading,
isError,
error,
} = useSalesChannel(id!)
if (isError) { if (isError) {
throw error throw error
@@ -1,6 +1,6 @@
import { PencilSquare, Trash } from "@medusajs/icons" import { PencilSquare, Trash } from "@medusajs/icons"
import { SalesChannelDTO } from "@medusajs/types" import { SalesChannelDTO } from "@medusajs/types"
import { Button, Container, Heading, usePrompt } from "@medusajs/ui" import { Button, Container, Heading, toast, usePrompt } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query" import { keepPreviousData } from "@tanstack/react-query"
import { createColumnHelper } from "@tanstack/react-table" import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react" import { useMemo } from "react"
@@ -24,12 +24,15 @@ export const SalesChannelListTable = () => {
const { raw, searchParams } = useSalesChannelTableQuery({ const { raw, searchParams } = useSalesChannelTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
}) })
const { sales_channels, count, isLoading, isError, error } = useSalesChannels( const {
searchParams, sales_channels,
{ count,
placeholderData: keepPreviousData, isPending: isLoading,
} isError,
) error,
} = useSalesChannels(searchParams, {
placeholderData: keepPreviousData,
})
const columns = useColumns() const columns = useColumns()
@@ -97,7 +100,18 @@ const SalesChannelActions = ({
return return
} }
await mutateAsync() try {
await mutateAsync()
toast.success(t("general.success"), {
description: t("salesChannels.toast.delete"),
dismissLabel: t("actions.close"),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
} }
return ( return (
@@ -1,5 +1,5 @@
import { Currency } from "@medusajs/medusa" import { Currency } from "@medusajs/medusa"
import { Button, Checkbox, Hint, Tooltip } from "@medusajs/ui" import { Button, Checkbox, Hint, toast, Tooltip } from "@medusajs/ui"
import { import {
OnChangeFn, OnChangeFn,
RowSelectionState, RowSelectionState,
@@ -68,12 +68,15 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
prefix: PREFIX, prefix: PREFIX,
}) })
const { currencies, count, isLoading, isError, error } = useCurrencies( const {
searchParams, currencies,
{ count,
placeholderData: keepPreviousData, isPending: isLoading,
} isError,
) error,
} = useCurrencies(searchParams, {
placeholderData: keepPreviousData,
})
const preSelectedRows = store.supported_currency_codes.map((c) => c) const preSelectedRows = store.supported_currency_codes.map((c) => c)
@@ -101,16 +104,21 @@ export const AddCurrenciesForm = ({ store }: AddCurrenciesFormProps) => {
new Set([...data.currencies, ...preSelectedRows]) new Set([...data.currencies, ...preSelectedRows])
) as string[] ) as string[]
await mutateAsync( try {
{ await mutateAsync({
supported_currency_codes: currencies, supported_currency_codes: currencies,
}, })
{ toast.success(t("general.success"), {
onSuccess: () => { description: t("store.toast.currenciesUpdated"),
handleSuccess() dismissLabel: t("actions.close"),
}, })
} handleSuccess()
) } catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
}) })
if (isError) { if (isError) {
@@ -3,7 +3,7 @@ import { useStore } from "../../../hooks/api/store"
import { AddCurrenciesForm } from "./components/add-currencies-form/add-currencies-form" import { AddCurrenciesForm } from "./components/add-currencies-form/add-currencies-form"
export const StoreAddCurrencies = () => { export const StoreAddCurrencies = () => {
const { store, isLoading, isError, error } = useStore() const { store, isPending: isLoading, isError, error } = useStore()
if (isError) { if (isError) {
throw error throw error
@@ -5,6 +5,7 @@ import {
CommandBar, CommandBar,
Container, Container,
Heading, Heading,
toast,
usePrompt, usePrompt,
} from "@medusajs/ui" } from "@medusajs/ui"
import { keepPreviousData } from "@tanstack/react-query" import { keepPreviousData } from "@tanstack/react-query"
@@ -31,7 +32,13 @@ export const StoreCurrencySection = ({ store }: StoreCurrencySectionProps) => {
const { searchParams, raw } = useCurrenciesTableQuery({ pageSize: PAGE_SIZE }) const { searchParams, raw } = useCurrenciesTableQuery({ pageSize: PAGE_SIZE })
const { currencies, count, isLoading, isError, error } = useCurrencies( const {
currencies,
count,
isPending: isLoading,
isError,
error,
} = useCurrencies(
{ {
code: store.supported_currency_codes, code: store.supported_currency_codes,
...searchParams, ...searchParams,
@@ -56,8 +63,9 @@ export const StoreCurrencySection = ({ store }: StoreCurrencySectionProps) => {
enableRowSelection: true, enableRowSelection: true,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
meta: { meta: {
currencyCodes: store.supported_currency_codes,
storeId: store.id, storeId: store.id,
currencyCodes: store.supported_currency_codes,
defaultCurrencyCode: store.default_currency_code,
}, },
}) })
@@ -81,18 +89,24 @@ export const StoreCurrencySection = ({ store }: StoreCurrencySectionProps) => {
return return
} }
await mutateAsync( try {
{ await mutateAsync({
supported_currency_codes: store.supported_currency_codes.filter( supported_currency_codes: store.supported_currency_codes.filter(
(c) => !ids.includes(c) (c) => !ids.includes(c)
), ),
}, })
{ setRowSelection({})
onSuccess: () => {
setRowSelection({}) toast.success(t("general.success"), {
}, description: t("store.toast.currenciesRemoved"),
} dismissLabel: t("actions.close"),
) })
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
} }
if (isError) { if (isError) {
@@ -151,10 +165,12 @@ const CurrencyActions = ({
storeId, storeId,
currency, currency,
currencyCodes, currencyCodes,
defaultCurrencyCode,
}: { }: {
storeId: string storeId: string
currency: CurrencyDTO currency: CurrencyDTO
currencyCodes: string[] currencyCodes: string[]
defaultCurrencyCode: string
}) => { }) => {
const { mutateAsync } = useUpdateStore(storeId) const { mutateAsync } = useUpdateStore(storeId)
@@ -177,11 +193,23 @@ const CurrencyActions = ({
return return
} }
await mutateAsync({ try {
supported_currency_codes: currencyCodes.filter( await mutateAsync({
(c) => c !== currency.code supported_currency_codes: currencyCodes.filter(
), (c) => c !== currency.code
}) ),
})
toast.success(t("general.success"), {
description: t("store.toast.currenciesRemoved"),
dismissLabel: t("actions.close"),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
} }
return ( return (
@@ -193,6 +221,7 @@ const CurrencyActions = ({
icon: <Trash />, icon: <Trash />,
label: t("actions.remove"), label: t("actions.remove"),
onClick: handleRemove, onClick: handleRemove,
disabled: currency.code === defaultCurrencyCode,
}, },
], ],
}, },
@@ -240,9 +269,11 @@ const useColumns = () => {
columnHelper.display({ columnHelper.display({
id: "actions", id: "actions",
cell: ({ row, table }) => { cell: ({ row, table }) => {
const { currencyCodes, storeId } = table.options.meta as { const { currencyCodes, storeId, defaultCurrencyCode } = table.options
.meta as {
currencyCodes: string[] currencyCodes: string[]
storeId: string storeId: string
defaultCurrencyCode: string
} }
return ( return (
@@ -250,6 +281,7 @@ const useColumns = () => {
storeId={storeId} storeId={storeId}
currency={row.original} currency={row.original}
currencyCodes={currencyCodes} currencyCodes={currencyCodes}
defaultCurrencyCode={defaultCurrencyCode}
/> />
) )
}, },
@@ -3,6 +3,7 @@ import { Badge, Container, Heading, Text } from "@medusajs/ui"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu" import { ActionMenu } from "../../../../../components/common/action-menu"
import { ExtendedStoreDTO } from "../../../../../types/api-responses" import { ExtendedStoreDTO } from "../../../../../types/api-responses"
import { useRegion } from "../../../../../hooks/api/regions"
type StoreGeneralSectionProps = { type StoreGeneralSectionProps = {
store: ExtendedStoreDTO store: ExtendedStoreDTO
@@ -11,6 +12,10 @@ type StoreGeneralSectionProps = {
export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => { export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { region } = useRegion(store.default_region_id, undefined, {
enabled: !!store.default_region_id,
})
return ( return (
<Container className="divide-y p-0"> <Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4"> <div className="flex items-center justify-between px-6 py-4">
@@ -57,6 +62,19 @@ export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
</div> </div>
</div> </div>
)} )}
{region && (
<div className="grid grid-cols-2 px-6 py-4">
<Text size="small" leading="compact" weight="plus">
{t("store.defaultRegion")}
</Text>
<div className="flex items-center gap-x-2">
<Text size="small" leading="compact">
{region.name}
</Text>
</div>
</div>
)}
</Container> </Container>
) )
} }
@@ -9,7 +9,12 @@ import { storeLoader } from "./loader"
export const StoreDetail = () => { export const StoreDetail = () => {
const initialData = useLoaderData() as Awaited<ReturnType<typeof storeLoader>> const initialData = useLoaderData() as Awaited<ReturnType<typeof storeLoader>>
const { store, isLoading, isError, error } = useStore({ const {
store,
isPending: isLoading,
isError,
error,
} = useStore({
initialData, initialData,
}) })
@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { Button, Input } from "@medusajs/ui" import { Button, Input, Select, toast } from "@medusajs/ui"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { z } from "zod" import { z } from "zod"
@@ -11,6 +11,7 @@ import {
} from "../../../../../components/route-modal" } from "../../../../../components/route-modal"
import { useUpdateStore } from "../../../../../hooks/api/store" import { useUpdateStore } from "../../../../../hooks/api/store"
import { ExtendedStoreDTO } from "../../../../../types/api-responses" import { ExtendedStoreDTO } from "../../../../../types/api-responses"
import { useRegions } from "../../../../../hooks/api/regions"
type EditStoreFormProps = { type EditStoreFormProps = {
store: ExtendedStoreDTO store: ExtendedStoreDTO
@@ -18,8 +19,8 @@ type EditStoreFormProps = {
const EditStoreSchema = z.object({ const EditStoreSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
// default_currency_code: z.string().optional(), default_currency_code: z.string().optional(),
// default_region_id: z.string().optional(), default_region_id: z.string().optional(),
// default_location_id: z.string().optional(), // default_location_id: z.string().optional(),
}) })
@@ -30,18 +31,32 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => {
const form = useForm<z.infer<typeof EditStoreSchema>>({ const form = useForm<z.infer<typeof EditStoreSchema>>({
defaultValues: { defaultValues: {
name: store.name, name: store.name,
default_region_id: store.default_region_id || undefined,
default_currency_code: store.default_currency_code || undefined,
}, },
resolver: zodResolver(EditStoreSchema), resolver: zodResolver(EditStoreSchema),
}) })
const { mutateAsync, isPending } = useUpdateStore(store.id) const { mutateAsync, isPending } = useUpdateStore(store.id)
const { regions, isPending: isRegionsLoading } = useRegions({ limit: 999 })
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
mutateAsync(values, { try {
onSuccess: () => { await mutateAsync(values)
handleSuccess()
}, handleSuccess()
})
toast.success(t("general.success"), {
description: t("store.toast.update"),
dismissLabel: t("actions.close"),
})
} catch (e) {
toast.error(t("general.error"), {
description: e.message,
dismissLabel: t("actions.close"),
})
}
}) })
return ( return (
@@ -62,7 +77,61 @@ export const EditStoreForm = ({ store }: EditStoreFormProps) => {
</Form.Item> </Form.Item>
)} )}
/> />
{/* TODO: Add comboboxes for default region, location, and currency. `q` is currently missing on all v2 endpoints */} {/* TODO: Add comboboxes for default sales channel and location */}
<Form.Field
control={form.control}
name="default_currency_code"
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label>{t("store.defaultCurrency")}</Form.Label>
<Form.Control>
<Select {...field} onValueChange={onChange}>
<Select.Trigger ref={field.ref}>
<Select.Value />
</Select.Trigger>
<Select.Content>
{store.supported_currency_codes.map((code) => (
<Select.Item key={code} value={code}>
{code.toUpperCase()}
</Select.Item>
))}
</Select.Content>
</Select>
</Form.Control>
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="default_region_id"
render={({ field: { onChange, ...field } }) => {
return (
<Form.Item>
<Form.Label>{t("store.defaultRegion")}</Form.Label>
<Form.Control>
<Select
{...field}
onValueChange={onChange}
disabled={isRegionsLoading}
>
<Select.Trigger ref={field.ref}>
<Select.Value />
</Select.Trigger>
<Select.Content>
{(regions || []).map((region) => (
<Select.Item key={region.id} value={region.id}>
{region.name}
</Select.Item>
))}
</Select.Content>
</Select>
</Form.Control>
</Form.Item>
)
}}
/>
</div> </div>
</RouteDrawer.Body> </RouteDrawer.Body>
<RouteDrawer.Footer> <RouteDrawer.Footer>
@@ -6,7 +6,7 @@ import { EditStoreForm } from "./components/edit-store-form/edit-store-form"
export const StoreEdit = () => { export const StoreEdit = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { store, isLoading, isError, error } = useStore() const { store, isPending: isLoading, isError, error } = useStore()
if (isError) { if (isError) {
throw error throw error
@@ -12,12 +12,12 @@ export const deleteStockLocationsStep = createStep(
return new StepResponse(void 0, input) return new StepResponse(void 0, input)
}, },
async (deletedLocaitonIds, { container }) => { async (deletedLocationIds, { container }) => {
if (!deletedLocaitonIds?.length) { if (!deletedLocationIds?.length) {
return return
} }
const service = container.resolve(ModuleRegistrationName.STOCK_LOCATION) const service = container.resolve(ModuleRegistrationName.STOCK_LOCATION)
await service.restore(deletedLocaitonIds) await service.restore(deletedLocationIds)
} }
) )
+7 -1
View File
@@ -1,9 +1,14 @@
import { BigNumberRawValue } from "@medusajs/types" import { BigNumberRawValue } from "@medusajs/types"
import { BigNumber, MikroOrmBigNumberProperty } from "@medusajs/utils" import {
BigNumber,
MikroOrmBigNumberProperty,
Searchable,
} from "@medusajs/utils"
import { Entity, PrimaryKey, Property } from "@mikro-orm/core" import { Entity, PrimaryKey, Property } from "@mikro-orm/core"
@Entity({ tableName: "currency" }) @Entity({ tableName: "currency" })
class Currency { class Currency {
@Searchable()
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
code!: string code!: string
@@ -13,6 +18,7 @@ class Currency {
@Property({ columnType: "text" }) @Property({ columnType: "text" })
symbol_native: string symbol_native: string
@Searchable()
@Property({ columnType: "text" }) @Property({ columnType: "text" })
name: string name: string
@@ -11,6 +11,7 @@ export const AdminGetCurrenciesParams = createFindParams({
limit: 50, limit: 50,
}).merge( }).merge(
z.object({ z.object({
q: z.string().optional(),
code: z.union([z.string(), z.array(z.string())]).optional(), code: z.union([z.string(), z.array(z.string())]).optional(),
$and: z.lazy(() => AdminGetCurrenciesParams.array()).optional(), $and: z.lazy(() => AdminGetCurrenciesParams.array()).optional(),
$or: z.lazy(() => AdminGetCurrenciesParams.array()).optional(), $or: z.lazy(() => AdminGetCurrenciesParams.array()).optional(),