Skip to content

Commit

Permalink
feat(dashboard): Users domain (#6212)
Browse files Browse the repository at this point in the history
  • Loading branch information
kasperkristensen authored Jan 30, 2024
1 parent 7d5a6f8 commit 1100c21
Show file tree
Hide file tree
Showing 16 changed files with 864 additions and 118 deletions.
24 changes: 22 additions & 2 deletions packages/admin-next/dashboard/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
"ascending": "Ascending",
"descending": "Descending",
"cancel": "Cancel",
"close": "Close",
"save": "Save",
"create": "Create",
"delete": "Delete",
"invite": "Invite",
"edit": "Edit",
"confirm": "Confirm",
"add": "Add",
Expand All @@ -28,6 +30,7 @@
"enabled": "Enabled",
"disabled": "Disabled",
"active": "Active",
"revoke": "Revoke",
"revoked": "Revoked",
"remove": "Remove",
"admin": "Admin",
Expand Down Expand Up @@ -126,7 +129,22 @@
},
"users": {
"domain": "Users",
"role": "Role",
"editUser": "Edit User",
"inviteUser": "Invite User",
"inviteUserHint": "Invite a new user to your store.",
"sendInvite": "Send invite",
"pendingInvites": "Pending Invites",
"revokeInviteWarning": "You are about to revoke the invite for {{email}}. This action cannot be undone.",
"resendInvite": "Resend invite",
"copyInviteLink": "Copy invite link",
"expiredOnDate": "Expired on {{date}}",
"validFromUntil": "Valid from <0>{{from}}</0> - <1>{{until}}</1>",
"acceptedOnDate": "Accepted on {{date}}",
"inviteStatus": {
"accepted": "Accepted",
"pending": "Pending",
"expired": "Expired"
},
"roles": {
"admin": "Admin",
"developer": "Developer",
Expand Down Expand Up @@ -244,6 +262,8 @@
"account": "Account",
"total": "Total",
"created": "Created",
"key": "Key"
"key": "Key",
"role": "Role",
"sent": "Sent"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Link } from "react-router-dom"
type Action = {
icon: ReactNode
label: string
disabled?: boolean
} & (
| {
to: string
Expand Down Expand Up @@ -47,12 +48,13 @@ export const ActionMenu = ({ groups }: ActionMenuProps) => {
if (action.onClick) {
return (
<DropdownMenu.Item
disabled={action.disabled}
key={index}
onClick={(e) => {
e.stopPropagation()
action.onClick()
}}
className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2"
className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{action.icon}
<span>{action.label}</span>
Expand All @@ -62,12 +64,16 @@ export const ActionMenu = ({ groups }: ActionMenuProps) => {

return (
<div key={index}>
<Link to={action.to} onClick={(e) => e.stopPropagation()}>
<DropdownMenu.Item className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2">
<DropdownMenu.Item
className="[&_svg]:text-ui-fg-subtle flex items-center gap-x-2 disabled:opacity-50 disabled:cursor-not-allowed"
asChild
disabled={action.disabled}
>
<Link to={action.to} onClick={(e) => e.stopPropagation()}>
{action.icon}
<span>{action.label}</span>
</DropdownMenu.Item>
</Link>
</Link>
</DropdownMenu.Item>
</div>
)
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { ExclamationCircle, MagnifyingGlass } from "@medusajs/icons"
import { Button, Text } from "@medusajs/ui"
import { Button, Text, clx } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"

type NoResultsProps = {
title?: string
message?: string
className?: string
}

export const NoResults = ({ title, message }: NoResultsProps) => {
export const NoResults = ({ title, message, className }: NoResultsProps) => {
const { t } = useTranslation()

return (
<div className="flex h-[400px] w-full items-center justify-center">
<div
className={clx(
"flex h-[400px] w-full items-center justify-center",
className
)}
>
<div className="flex flex-col items-center gap-y-2">
<MagnifyingGlass />
<Text size="small" leading="compact" weight="plus">
Expand All @@ -33,13 +39,24 @@ type NoRecordsProps = {
to: string
label: string
}
className?: string
}

export const NoRecords = ({ title, message, action }: NoRecordsProps) => {
export const NoRecords = ({
title,
message,
action,
className,
}: NoRecordsProps) => {
const { t } = useTranslation()

return (
<div className="flex h-[400px] w-full flex-col items-center justify-center gap-y-6">
<div
className={clx(
"flex h-[400px] w-full flex-col items-center justify-center gap-y-6",
className
)}
>
<div className="flex flex-col items-center gap-y-2">
<ExclamationCircle />
<Text size="small" leading="compact" weight="plus">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
AdminPublishableApiKeysRes,
AdminRegionsRes,
AdminSalesChannelsRes,
AdminUserRes,
} from "@medusajs/medusa"
import {
Outlet,
Expand Down Expand Up @@ -439,6 +440,9 @@ const router = createBrowserRouter([
{
path: ":id",
lazy: () => import("../../routes/users/user-detail"),
handle: {
crumb: (data: AdminUserRes) => data.user.email,
},
children: [
{
path: "edit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export const ProfileGeneralSection = ({ user }: ProfileGeneralSectionProps) => {
{user.email}
</Text>
</div>
<div className="grid grid-cols-2 px-6 py-4 items-center">
<Text size="small" leading="compact" weight="plus">
{t("fields.role")}
</Text>
<Text size="small" leading="compact">
{t(`users.roles.${user.role}`)}
</Text>
</div>
<div className="grid grid-cols-2 px-6 py-4 items-center">
<Text size="small" leading="compact" weight="plus">
{t("profile.language")}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { User } from "@medusajs/medusa"
import { Button, Container, Heading, Text } from "@medusajs/ui"
import { Button, Container, Heading, Text, clx } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"

Expand All @@ -9,35 +9,39 @@ type UserGeneralSection = {

export const UserGeneralSection = ({ user }: UserGeneralSection) => {
const { t } = useTranslation()

const name = [user.first_name, user.last_name].filter(Boolean).join(" ")

return (
<Container className="divide-y p-0">
<Container className="p-0 divide-y">
<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>
<Heading>{user.email}</Heading>
<Link to={`edit`}>
<Button size="small" variant="secondary">
{t("profile.editProfile")}
{t("general.edit")}
</Button>
</Link>
</div>
<div className="grid grid-cols-2 px-6 py-4">
<div className="grid grid-cols-2 px-6 py-4 items-center">
<Text size="small" leading="compact" weight="plus">
{t("fields.name")}
</Text>
<Text size="small" leading="compact">
{user.first_name} {user.last_name}
<Text
size="small"
leading="compact"
className={clx({
"text-ui-fg-subtle": !name,
})}
>
{name ?? "-"}
</Text>
</div>
<div className="grid grid-cols-2 px-6 py-4">
<div className="grid grid-cols-2 px-6 py-4 items-center">
<Text size="small" leading="compact" weight="plus">
{t("fields.email")}
{t("fields.role")}
</Text>
<Text size="small" leading="compact">
{user.email}
{t(`users.roles.${user.role}`)}
</Text>
</div>
</Container>
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { userLoader as loader } from "./loader"
export { UserDetail as Component } from "./user-detail"
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AdminUserRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { adminProductKeys } from "medusa-react"
import { LoaderFunctionArgs } from "react-router-dom"

import { medusa, queryClient } from "../../../lib/medusa"

const userDetailQuery = (id: string) => ({
queryKey: adminProductKeys.detail(id),
queryFn: async () => medusa.admin.users.retrieve(id),
})

export const userLoader = async ({ params }: LoaderFunctionArgs) => {
const id = params.id
const query = userDetailQuery(id!)

return (
queryClient.getQueryData<Response<AdminUserRes>>(query.queryKey) ??
(await queryClient.fetchQuery(query))
)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { useAdminUser } from "medusa-react"
import { Outlet, json, useParams } from "react-router-dom"
import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import { UserGeneralSection } from "./components/user-general-section"
import { userLoader } from "./loader"

export const UserDetail = () => {
const initialData = useLoaderData() as Awaited<ReturnType<typeof userLoader>>

const { id } = useParams()
const { user, isLoading, isError, error } = useAdminUser(id!)
const { user, isLoading, isError, error } = useAdminUser(id!, {
initialData,
})

if (isLoading) {
return <div>Loading...</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { User } from "@medusajs/medusa"
import { Button, Drawer, Input } from "@medusajs/ui"
import { useAdminUpdateUser } from "medusa-react"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"

type EditUserFormProps = {
user: Omit<User, "password_hash">
subscribe: (state: boolean) => void
onSuccessfulSubmit: () => void
}

const EditUserFormSchema = zod.object({
first_name: zod.string().optional(),
last_name: zod.string().optional(),
})

export const EditUserForm = ({
user,
subscribe,
onSuccessfulSubmit,
}: EditUserFormProps) => {
const form = useForm<zod.infer<typeof EditUserFormSchema>>({
defaultValues: {
first_name: user.first_name || "",
last_name: user.last_name || "",
},
resolver: zodResolver(EditUserFormSchema),
})

const {
formState: { isDirty },
} = form

useEffect(() => {
subscribe(isDirty)
}, [isDirty])

const { t } = useTranslation()

const { mutateAsync, isLoading } = useAdminUpdateUser(user.id)

const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync(values, {
onSuccess: () => {
onSuccessfulSubmit()
},
})
})

return (
<Form {...form}>
<form
onSubmit={handleSubmit}
className="flex flex-col overflow-hidden flex-1"
>
<Drawer.Body className="flex flex-col gap-y-8 overflow-y-auto flex-1 max-w-full">
<Form.Field
control={form.control}
name="first_name"
render={({ field }) => {
return (
<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 }) => {
return (
<Form.Item>
<Form.Label>{t("fields.lastName")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</Drawer.Body>
<Drawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<Button size="small" variant="secondary">
{t("general.cancel")}
</Button>
</Drawer.Close>
<Button size="small" type="submit" isLoading={isLoading}>
{t("general.save")}
</Button>
</div>
</Drawer.Footer>
</form>
</Form>
)
}
Loading

0 comments on commit 1100c21

Please sign in to comment.