Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): Users domain #6212

Merged
merged 7 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading