feat(admin-sdk,admin-bundler,admin-shared,medusa): Restructure admin packages (#8988)
**What** - Renames /admin-next -> /admin - Renames @medusajs/admin-sdk -> @medusajs/admin-bundler - Creates a new package called @medusajs/admin-sdk that will hold all tooling relevant to creating admin extensions. This is currently `defineRouteConfig` and `defineWidgetConfig`, but will eventually also export methods for adding custom fields, register translation, etc. - cc: @shahednasser we should update the examples in the docs so these functions are imported from `@medusajs/admin-sdk`. People will also need to install the package in their project, as it's no longer a transient dependency. - cc: @olivermrbl we might want to publish a changelog when this is merged, as it is a breaking change, and will require people to import the `defineXConfig` from the new package instead of `@medusajs/admin-shared`. - Updates CODEOWNERS so /admin packages does not require a review from the UI team.
This commit is contained in:
+1
@@ -0,0 +1 @@
|
||||
export * from "./profile-general-section"
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { PencilSquare } from "@medusajs/icons"
|
||||
import { UserDTO } from "@medusajs/types"
|
||||
import { Container, Heading, StatusBadge, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { languages } from "../../../../../i18n/languages"
|
||||
|
||||
type ProfileGeneralSectionProps = {
|
||||
user: UserDTO
|
||||
}
|
||||
|
||||
export const ProfileGeneralSection = ({ user }: ProfileGeneralSectionProps) => {
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const name = [user.first_name, user.last_name].filter(Boolean).join(" ")
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<div>
|
||||
<Heading>{t("profile.domain")}</Heading>
|
||||
<Text className="text-ui-fg-subtle" size="small">
|
||||
{t("profile.manageYourProfileDetails")}
|
||||
</Text>
|
||||
</div>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("actions.edit"),
|
||||
to: "edit",
|
||||
icon: <PencilSquare />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.name")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{name || "-"}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("fields.email")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{user.email}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("profile.fields.languageLabel")}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{languages.find((lang) => lang.code === i18n.language)
|
||||
?.display_name || "-"}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center px-6 py-4">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("profile.fields.usageInsightsLabel")}
|
||||
</Text>
|
||||
<StatusBadge color="red" className="w-fit">
|
||||
{t("general.disabled")}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ProfileDetail as Component } from "./profile-detail"
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Outlet, json } from "react-router-dom"
|
||||
|
||||
import { useMe } from "../../../hooks/api/users"
|
||||
import { ProfileGeneralSection } from "./components/profile-general-section"
|
||||
|
||||
import after from "virtual:medusa/widgets/profile/details/after"
|
||||
import before from "virtual:medusa/widgets/profile/details/before"
|
||||
|
||||
export const ProfileDetail = () => {
|
||||
const { user, isPending: isLoading, isError, error } = useMe()
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (isError || !user) {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw json("An unknown error has occurred", 500)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{before.widgets.map((w, i) => (
|
||||
<div key={i}>
|
||||
<w.Component data={user} />
|
||||
</div>
|
||||
))}
|
||||
<ProfileGeneralSection user={user} />
|
||||
{after.widgets.map((w, i) => (
|
||||
<div key={i}>
|
||||
<w.Component data={user} />
|
||||
</div>
|
||||
))}
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Button, Input, Select, Switch, toast } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { UserDTO } from "@medusajs/types"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||
import { useUpdateUser } from "../../../../../hooks/api/users"
|
||||
import { languages } from "../../../../../i18n/languages"
|
||||
|
||||
type EditProfileProps = {
|
||||
user: Partial<Omit<UserDTO, "password_hash">>
|
||||
usageInsights: boolean
|
||||
}
|
||||
|
||||
const EditProfileSchema = zod.object({
|
||||
first_name: zod.string().optional(),
|
||||
last_name: zod.string().optional(),
|
||||
language: zod.string(),
|
||||
usage_insights: zod.boolean(),
|
||||
})
|
||||
|
||||
export const EditProfileForm = ({ user, usageInsights }: EditProfileProps) => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<zod.infer<typeof EditProfileSchema>>({
|
||||
defaultValues: {
|
||||
first_name: user.first_name ?? "",
|
||||
last_name: user.last_name ?? "",
|
||||
language: i18n.language,
|
||||
usage_insights: usageInsights,
|
||||
},
|
||||
resolver: zodResolver(EditProfileSchema),
|
||||
})
|
||||
|
||||
const changeLanguage = async (code: string) => {
|
||||
await i18n.changeLanguage(code)
|
||||
}
|
||||
|
||||
const sortedLanguages = languages.sort((a, b) =>
|
||||
a.display_name.localeCompare(b.display_name)
|
||||
)
|
||||
|
||||
const { mutateAsync, isPending } = useUpdateUser(user.id!)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
first_name: values.first_name,
|
||||
last_name: values.last_name,
|
||||
})
|
||||
|
||||
await changeLanguage(values.language)
|
||||
|
||||
toast.success(t("profile.toast.edit"))
|
||||
handleSuccess()
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||
<RouteDrawer.Body>
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="first_name"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.firstName")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="last_name"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.lastName")}</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="language"
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<Form.Item className="gap-y-4">
|
||||
<div>
|
||||
<Form.Label>{t("profile.fields.languageLabel")}</Form.Label>
|
||||
<Form.Hint>{t("profile.edit.languageHint")}</Form.Hint>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Control>
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<Select.Trigger ref={ref} className="py-1 text-[13px]">
|
||||
<Select.Value
|
||||
placeholder={t("profile.edit.languagePlaceholder")}
|
||||
>
|
||||
{
|
||||
sortedLanguages.find(
|
||||
(language) => language.code === field.value
|
||||
)?.display_name
|
||||
}
|
||||
</Select.Value>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{languages.map((language) => (
|
||||
<Select.Item
|
||||
key={language.code}
|
||||
value={language.code}
|
||||
>
|
||||
{language.display_name}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="usage_insights"
|
||||
render={({ field: { value, onChange, ...rest } }) => (
|
||||
<Form.Item>
|
||||
<div className="flex items-center justify-between">
|
||||
<Form.Label>
|
||||
{t("profile.fields.usageInsightsLabel")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Switch
|
||||
{...rest}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Hint>
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey="profile.edit.usageInsightsHint"
|
||||
components={[
|
||||
<a
|
||||
key="hint-link"
|
||||
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover transition-fg underline"
|
||||
// TODO change link once docs are public
|
||||
href="https://medusa-resources-git-docs-v2-medusajs.vercel.app/resources/usage#admin-analytics"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</span>
|
||||
</Form.Hint>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<Button size="small" type="submit" isLoading={isPending}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ProfileEdit as Component } from "./profile-edit"
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { RouteDrawer } from "../../../components/modals"
|
||||
import { useMe } from "../../../hooks/api/users"
|
||||
import { EditProfileForm } from "./components/edit-profile-form/edit-profile-form"
|
||||
|
||||
export const ProfileEdit = () => {
|
||||
const { user, isPending: isLoading, isError, error } = useMe()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header className="capitalize">
|
||||
<Heading>{t("profile.edit.header")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && user && (
|
||||
<EditProfileForm user={user} usageInsights={false} />
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user