feat(dashboard): Pub Api Key domain (#6162)
**What** - Adds Pub. Api Key domain **Note** - Pagination for sales channels associated with the pubkey is not implemented correctly as it is not supported by the API. Will open a separate issue on this, and revisit the impl. once fixed. CLOSES CORE-1656
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/ui": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feature(ui): Adds a `size` variant to `<Copy />` component, and prevent clicks from propigating to parent elements". Also adds additional sizes to the `<Avatar />` component.
|
||||||
@@ -27,6 +27,8 @@
|
|||||||
"details": "Details",
|
"details": "Details",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
|
"active": "Active",
|
||||||
|
"revoked": "Revoked",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"store": "Store",
|
"store": "Store",
|
||||||
@@ -183,10 +185,16 @@
|
|||||||
"domain": "API Key Management",
|
"domain": "API Key Management",
|
||||||
"createKey": "Create key",
|
"createKey": "Create key",
|
||||||
"createPublishableApiKey": "Create Publishable API Key",
|
"createPublishableApiKey": "Create Publishable API Key",
|
||||||
|
"editKey": "Edit key",
|
||||||
"revoke": "Revoke",
|
"revoke": "Revoke",
|
||||||
"publishableApiKeyHint": "Publishable API keys are used to limit the scope of requests to specific sales channels.",
|
"publishableApiKeyHint": "Publishable API keys are used to limit the scope of requests to specific sales channels.",
|
||||||
"deleteKeyWarning": "You are about to delete the API key {{title}}. This action cannot be undone.",
|
"deleteKeyWarning": "You are about to delete the API key {{title}}. This action cannot be undone.",
|
||||||
"revokeKeyWarning": "You are about to revoke the API key {{title}}."
|
"revokeKeyWarning": "You are about to revoke the API key {{title}}. This action cannot be undone, and the key cannot be used in future requests.",
|
||||||
|
"removeSalesChannelWarning": "You are about to remove the sales channel {{name}} from the API key. This action cannot be undone.",
|
||||||
|
"removeSalesChannelsWarning_one": "You are about to remove {{count}} sales channel from the API key. This action cannot be undone.",
|
||||||
|
"removeSalesChannelsWarning_other": "You are about to remove {{count}} sales channels from the API key. This action cannot be undone.",
|
||||||
|
"createdBy": "Created by",
|
||||||
|
"revokedBy": "Revoked by"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
@@ -234,6 +242,8 @@
|
|||||||
"variants": "Variants",
|
"variants": "Variants",
|
||||||
"orders": "Orders",
|
"orders": "Orders",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"total": "Total"
|
"total": "Total",
|
||||||
|
"created": "Created",
|
||||||
|
"key": "Key"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./user-link"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Avatar, Text } from "@medusajs/ui"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
type UserLinkProps = {
|
||||||
|
id: string
|
||||||
|
first_name?: string | null
|
||||||
|
last_name?: string | null
|
||||||
|
email: string
|
||||||
|
type?: "customer" | "user"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserLink = ({
|
||||||
|
id,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
email,
|
||||||
|
type = "user",
|
||||||
|
}: UserLinkProps) => {
|
||||||
|
const name = [first_name, last_name].filter(Boolean).join(" ")
|
||||||
|
const fallback = name ? name.slice(0, 1) : email.slice(0, 1)
|
||||||
|
const link = type === "user" ? `/settings/users/${id}` : `/customers/${id}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={link}
|
||||||
|
className="flex items-center gap-x-2 w-fit transition-fg hover:text-ui-fg-subtle outline-none focus-visible:shadow-borders-focus rounded-md"
|
||||||
|
>
|
||||||
|
<Avatar size="2xsmall" fallback={fallback.toUpperCase()} />
|
||||||
|
<Text size="small" leading="compact" weight="regular">
|
||||||
|
{name || email}
|
||||||
|
</Text>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ import type {
|
|||||||
AdminCustomerGroupsRes,
|
AdminCustomerGroupsRes,
|
||||||
AdminCustomersRes,
|
AdminCustomersRes,
|
||||||
AdminProductsRes,
|
AdminProductsRes,
|
||||||
|
AdminPublishableApiKeysRes,
|
||||||
AdminRegionsRes,
|
AdminRegionsRes,
|
||||||
|
AdminSalesChannelsRes,
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
import {
|
import {
|
||||||
Outlet,
|
Outlet,
|
||||||
@@ -487,6 +489,10 @@ const router = createBrowserRouter([
|
|||||||
path: ":id",
|
path: ":id",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import("../../routes/sales-channels/sales-channel-detail"),
|
import("../../routes/sales-channels/sales-channel-detail"),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: AdminSalesChannelsRes) =>
|
||||||
|
data.sales_channel.name,
|
||||||
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "edit",
|
path: "edit",
|
||||||
@@ -533,6 +539,10 @@ const router = createBrowserRouter([
|
|||||||
import(
|
import(
|
||||||
"../../routes/api-key-management/api-key-management-detail"
|
"../../routes/api-key-management/api-key-management-detail"
|
||||||
),
|
),
|
||||||
|
handle: {
|
||||||
|
crumb: (data: AdminPublishableApiKeysRes) =>
|
||||||
|
data.publishable_api_key.title,
|
||||||
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "edit",
|
path: "edit",
|
||||||
@@ -541,6 +551,13 @@ const router = createBrowserRouter([
|
|||||||
"../../routes/api-key-management/api-key-management-edit"
|
"../../routes/api-key-management/api-key-management-edit"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "add-sales-channels",
|
||||||
|
lazy: () =>
|
||||||
|
import(
|
||||||
|
"../../routes/api-key-management/api-key-management-add-sales-channels"
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import { FocusModal } from "@medusajs/ui"
|
||||||
|
import { useAdminPublishableApiKeySalesChannels } from "medusa-react"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
|
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||||
|
import { AddSalesChannelsToApiKeyForm } from "./components"
|
||||||
|
|
||||||
|
export const ApiKeyManagementAddSalesChannels = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||||
|
|
||||||
|
const { sales_channels, isLoading, isError, error } =
|
||||||
|
useAdminPublishableApiKeySalesChannels(id!)
|
||||||
|
|
||||||
|
const handleSuccessfulSubmit = () => {
|
||||||
|
onOpenChange(false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusModal open={open} onOpenChange={onOpenChange}>
|
||||||
|
<FocusModal.Content>
|
||||||
|
{!isLoading && sales_channels && (
|
||||||
|
<AddSalesChannelsToApiKeyForm
|
||||||
|
apiKey={id!}
|
||||||
|
preSelected={sales_channels.map((sc) => sc.id)}
|
||||||
|
onSuccessfulSubmit={handleSuccessfulSubmit}
|
||||||
|
subscribe={subscribe}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FocusModal.Content>
|
||||||
|
</FocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
+326
@@ -0,0 +1,326 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { SalesChannel } from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FocusModal,
|
||||||
|
Hint,
|
||||||
|
StatusBadge,
|
||||||
|
Table,
|
||||||
|
Tooltip,
|
||||||
|
clx,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
PaginationState,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
useAdminAddPublishableKeySalesChannelsBatch,
|
||||||
|
useAdminSalesChannels,
|
||||||
|
} from "medusa-react"
|
||||||
|
import { useEffect, useMemo, useState } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import * as zod from "zod"
|
||||||
|
import { Form } from "../../../../components/common/form"
|
||||||
|
import { OrderBy } from "../../../../components/filtering/order-by"
|
||||||
|
import { Query } from "../../../../components/filtering/query"
|
||||||
|
import { LocalizedTablePagination } from "../../../../components/localization/localized-table-pagination"
|
||||||
|
import { useQueryParams } from "../../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
type AddSalesChannelsToApiKeyFormProps = {
|
||||||
|
apiKey: string
|
||||||
|
preSelected: string[]
|
||||||
|
subscribe: (state: boolean) => void
|
||||||
|
onSuccessfulSubmit: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddSalesChannelsToApiKeySchema = zod.object({
|
||||||
|
sales_channel_ids: zod.array(zod.string()).min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
const PAGE_SIZE = 50
|
||||||
|
|
||||||
|
export const AddSalesChannelsToApiKeyForm = ({
|
||||||
|
apiKey,
|
||||||
|
preSelected,
|
||||||
|
subscribe,
|
||||||
|
onSuccessfulSubmit,
|
||||||
|
}: AddSalesChannelsToApiKeyFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof AddSalesChannelsToApiKeySchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
sales_channel_ids: [],
|
||||||
|
},
|
||||||
|
resolver: zodResolver(AddSalesChannelsToApiKeySchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
formState: { isDirty },
|
||||||
|
} = form
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
subscribe(isDirty)
|
||||||
|
}, [isDirty])
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading: isMutating } =
|
||||||
|
useAdminAddPublishableKeySalesChannelsBatch(apiKey)
|
||||||
|
|
||||||
|
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = useMemo(
|
||||||
|
() => ({
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
}),
|
||||||
|
[pageIndex, pageSize]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setValue(
|
||||||
|
"sales_channel_ids",
|
||||||
|
Object.keys(rowSelection).filter((k) => rowSelection[k])
|
||||||
|
)
|
||||||
|
}, [rowSelection])
|
||||||
|
|
||||||
|
const params = useQueryParams(["q", "order"])
|
||||||
|
const { sales_channels, count } = useAdminSalesChannels(
|
||||||
|
{
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
offset: PAGE_SIZE * pageIndex,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: sales_channels ?? [],
|
||||||
|
columns,
|
||||||
|
pageCount: Math.ceil((count ?? 0) / PAGE_SIZE),
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
enableRowSelection: (row) => {
|
||||||
|
return !preSelected.includes(row.id)
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
preSelected,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
sales_channel_ids: values.sales_channel_ids.map((p) => ({ id: p })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
onSuccessfulSubmit()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex h-full flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<FocusModal.Header>
|
||||||
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
|
{form.formState.errors.sales_channel_ids && (
|
||||||
|
<Hint variant="error">
|
||||||
|
{form.formState.errors.sales_channel_ids.message}
|
||||||
|
</Hint>
|
||||||
|
)}
|
||||||
|
<FocusModal.Close asChild>
|
||||||
|
<Button size="small" variant="secondary">
|
||||||
|
{t("general.cancel")}
|
||||||
|
</Button>
|
||||||
|
</FocusModal.Close>
|
||||||
|
<Button size="small" type="submit" isLoading={isMutating}>
|
||||||
|
{t("general.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FocusModal.Header>
|
||||||
|
<FocusModal.Body className="flex h-full w-full flex-col items-center overflow-y-auto divide-y">
|
||||||
|
<div className="flex items-center justify-between w-full px-6 py-4">
|
||||||
|
<div></div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Query />
|
||||||
|
<OrderBy keys={["title"]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex-1 overflow-y-auto">
|
||||||
|
<Table>
|
||||||
|
<Table.Header className="border-t-0">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => {
|
||||||
|
return (
|
||||||
|
<Table.Row
|
||||||
|
key={headerGroup.id}
|
||||||
|
className="[&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<Table.HeaderCell key={header.id}>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</Table.HeaderCell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Row>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body className="border-b-0">
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<Table.Row
|
||||||
|
key={row.id}
|
||||||
|
className={clx(
|
||||||
|
"transition-fg",
|
||||||
|
{
|
||||||
|
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||||
|
row.getIsSelected(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bg-ui-bg-disabled hover:bg-ui-bg-disabled":
|
||||||
|
preSelected.includes(row.original.id),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<Table.Cell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
))}
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div className="w-full border-t">
|
||||||
|
<LocalizedTablePagination
|
||||||
|
canNextPage={table.getCanNextPage()}
|
||||||
|
canPreviousPage={table.getCanPreviousPage()}
|
||||||
|
nextPage={table.nextPage}
|
||||||
|
previousPage={table.previousPage}
|
||||||
|
count={count ?? 0}
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
pageCount={table.getPageCount()}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FocusModal.Body>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<SalesChannel>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row, table }) => {
|
||||||
|
const { preSelected } = table.options.meta as {
|
||||||
|
preSelected: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdded = preSelected.includes(row.original.id)
|
||||||
|
const isSelected = row.getIsSelected() || isAdded
|
||||||
|
|
||||||
|
const Component = (
|
||||||
|
<Checkbox
|
||||||
|
checked={isSelected}
|
||||||
|
disabled={isAdded}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isAdded) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
content={t("salesChannels.productAlreadyAdded")}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
{Component}
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Component
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: t("fields.name"),
|
||||||
|
cell: ({ getValue }) => getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("description", {
|
||||||
|
header: t("fields.description"),
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<div className="w-[200px] truncate">
|
||||||
|
<span>{getValue()}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("is_disabled", {
|
||||||
|
header: t("fields.status"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StatusBadge color={value ? "grey" : "green"}>
|
||||||
|
{value ? t("general.disabled") : t("general.enabled")}
|
||||||
|
</StatusBadge>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./add-sales-channels-to-api-key-form"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ApiKeyManagementAddSalesChannels as Component } from "./api-key-management-add-sales-channels"
|
||||||
+1
-1
@@ -8,7 +8,7 @@ export const ApiKeyManagementCreate = () => {
|
|||||||
return (
|
return (
|
||||||
<FocusModal open={open} onOpenChange={onOpenChange}>
|
<FocusModal open={open} onOpenChange={onOpenChange}>
|
||||||
<FocusModal.Content>
|
<FocusModal.Content>
|
||||||
<CreatePublishableApiKeyForm />
|
<CreatePublishableApiKeyForm subscribe={subscribe} />
|
||||||
</FocusModal.Content>
|
</FocusModal.Content>
|
||||||
</FocusModal>
|
</FocusModal>
|
||||||
)
|
)
|
||||||
|
|||||||
+9
-1
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"
|
|||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
|
|
||||||
type CreatePublishableApiKeyFormProps = {
|
type CreatePublishableApiKeyFormProps = {
|
||||||
@@ -37,9 +38,16 @@ export const CreatePublishableApiKeyForm = ({
|
|||||||
}, [isDirty])
|
}, [isDirty])
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
await mutateAsync(values)
|
await mutateAsync(values, {
|
||||||
|
onSuccess: ({ publishable_api_key }) => {
|
||||||
|
navigate(`/settings/api-key-management/${publishable_api_key.id}`, {
|
||||||
|
replace: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+37
-1
@@ -1,3 +1,39 @@
|
|||||||
|
import { useAdminPublishableApiKey } from "medusa-react"
|
||||||
|
import { Outlet, json, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
|
import { ApiKeyGeneralSection } from "./components/api-key-general-section"
|
||||||
|
import { ApiKeySalesChannelSection } from "./components/api-key-sales-channel-section"
|
||||||
|
import { apiKeyLoader } from "./loader"
|
||||||
|
|
||||||
export const ApiKeyManagementDetail = () => {
|
export const ApiKeyManagementDetail = () => {
|
||||||
return <div className="flex flex-col gap-y-2"></div>
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof apiKeyLoader>
|
||||||
|
>
|
||||||
|
|
||||||
|
const { id } = useParams()
|
||||||
|
const { publishable_api_key, isLoading, isError, error } =
|
||||||
|
useAdminPublishableApiKey(id!, {
|
||||||
|
initialData,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError || !publishable_api_key) {
|
||||||
|
if (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
throw json("An unknown error occurred", 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<ApiKeyGeneralSection apiKey={publishable_api_key} />
|
||||||
|
<ApiKeySalesChannelSection apiKey={publishable_api_key} />
|
||||||
|
<JsonViewSection data={publishable_api_key} />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+180
@@ -0,0 +1,180 @@
|
|||||||
|
import { PencilSquare, Trash, XCircle } from "@medusajs/icons"
|
||||||
|
import { PublishableApiKey } from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Copy,
|
||||||
|
Heading,
|
||||||
|
StatusBadge,
|
||||||
|
Text,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
useAdminDeletePublishableApiKey,
|
||||||
|
useAdminRevokePublishableApiKey,
|
||||||
|
useAdminUser,
|
||||||
|
} from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { Skeleton } from "../../../../../components/common/skeleton"
|
||||||
|
import { UserLink } from "../../../../../components/common/user-link"
|
||||||
|
|
||||||
|
type ApiKeyGeneralSectionProps = {
|
||||||
|
apiKey: PublishableApiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiKeyGeneralSection = ({ apiKey }: ApiKeyGeneralSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const { mutateAsync: revokeAsync } = useAdminRevokePublishableApiKey(
|
||||||
|
apiKey.id
|
||||||
|
)
|
||||||
|
const { mutateAsync: deleteAsync } = useAdminDeletePublishableApiKey(
|
||||||
|
apiKey.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("apiKeyManagement.deleteKeyWarning", {
|
||||||
|
title: apiKey.title,
|
||||||
|
}),
|
||||||
|
confirmText: t("general.delete"),
|
||||||
|
cancelText: t("general.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRevoke = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("apiKeyManagement.revokeKeyWarning", {
|
||||||
|
title: apiKey.title,
|
||||||
|
}),
|
||||||
|
confirmText: t("apiKeyManagement.revoke"),
|
||||||
|
cancelText: t("general.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await revokeAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
const dangerousActions = [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("general.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!apiKey.revoked_at) {
|
||||||
|
dangerousActions.unshift({
|
||||||
|
icon: <XCircle />,
|
||||||
|
label: t("apiKeyManagement.revoke"),
|
||||||
|
onClick: handleRevoke,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="p-0 divide-y">
|
||||||
|
<div className="px-6 py-4 flex items-center justify-between">
|
||||||
|
<Heading>{apiKey.title}</Heading>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<StatusBadge color={apiKey.revoked_at ? "red" : "green"}>
|
||||||
|
{apiKey.revoked_at ? t("general.revoked") : t("general.active")}
|
||||||
|
</StatusBadge>
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("general.edit"),
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
to: `/settings/api-key-management/${apiKey.id}/edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: dangerousActions,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 px-6 py-4 items-center">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("fields.key")}
|
||||||
|
</Text>
|
||||||
|
<div className="bg-ui-bg-subtle border border-ui-border-base flex items-center gap-x-0.5 w-fit rounded-full pl-2 pr-1 box-border cursor-default overflow-hidden">
|
||||||
|
<Text size="xsmall" leading="compact" className="truncate">
|
||||||
|
{apiKey.id}
|
||||||
|
</Text>
|
||||||
|
<Copy
|
||||||
|
content={apiKey.id}
|
||||||
|
variant="mini"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 px-6 py-4 items-center">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("apiKeyManagement.createdBy")}
|
||||||
|
</Text>
|
||||||
|
<ActionBy userId={apiKey.created_by} />
|
||||||
|
</div>
|
||||||
|
{apiKey.revoked_at && (
|
||||||
|
<div className="grid grid-cols-2 px-6 py-4 items-center">
|
||||||
|
<Text size="small" leading="compact" weight="plus">
|
||||||
|
{t("apiKeyManagement.revokedBy")}
|
||||||
|
</Text>
|
||||||
|
<ActionBy userId={apiKey.revoked_by} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActionBy = ({ userId }: { userId: string | null }) => {
|
||||||
|
const { user, isLoading, isError, error } = useAdminUser(userId!, {
|
||||||
|
enabled: !!userId,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return (
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
-
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-[20px_1fr]">
|
||||||
|
<Skeleton className="w-5 h-5 rounded-full" />
|
||||||
|
<Skeleton className="max-w-[220px] w-full" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
|
-
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <UserLink {...user} />
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./api-key-general-section"
|
||||||
+368
@@ -0,0 +1,368 @@
|
|||||||
|
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||||
|
import { PublishableApiKey, SalesChannel } from "@medusajs/medusa"
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
CommandBar,
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
StatusBadge,
|
||||||
|
Table,
|
||||||
|
clx,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import {
|
||||||
|
PaginationState,
|
||||||
|
RowSelectionState,
|
||||||
|
createColumnHelper,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table"
|
||||||
|
import {
|
||||||
|
useAdminPublishableApiKeySalesChannels,
|
||||||
|
useAdminRemovePublishableKeySalesChannelsBatch,
|
||||||
|
} from "medusa-react"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link, useNavigate } from "react-router-dom"
|
||||||
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import {
|
||||||
|
NoRecords,
|
||||||
|
NoResults,
|
||||||
|
} from "../../../../../components/common/empty-table-content"
|
||||||
|
import { Query } from "../../../../../components/filtering/query"
|
||||||
|
import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination"
|
||||||
|
import { useQueryParams } from "../../../../../hooks/use-query-params"
|
||||||
|
|
||||||
|
type ApiKeySalesChannelSectionProps = {
|
||||||
|
apiKey: PublishableApiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
export const ApiKeySalesChannelSection = ({
|
||||||
|
apiKey,
|
||||||
|
}: ApiKeySalesChannelSectionProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pagination = useMemo(
|
||||||
|
() => ({
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
}),
|
||||||
|
[pageIndex, pageSize]
|
||||||
|
)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
|
const params = useQueryParams(["q"])
|
||||||
|
const { sales_channels, isLoading, isError, error } =
|
||||||
|
useAdminPublishableApiKeySalesChannels(
|
||||||
|
apiKey.id,
|
||||||
|
{
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const count = sales_channels?.length || 0
|
||||||
|
|
||||||
|
const columns = useColumns()
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: sales_channels ?? [],
|
||||||
|
columns,
|
||||||
|
pageCount: Math.ceil(count / PAGE_SIZE),
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
getRowId: (row) => row.id,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
meta: {
|
||||||
|
apiKey: apiKey.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutateAsync } = useAdminRemovePublishableKeySalesChannelsBatch(
|
||||||
|
apiKey.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleRemove = async () => {
|
||||||
|
const keys = Object.keys(rowSelection).filter((k) => rowSelection[k])
|
||||||
|
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("apiKeyManagement.removeSalesChannelsWarning", {
|
||||||
|
count: keys.length,
|
||||||
|
}),
|
||||||
|
confirmText: t("general.continue"),
|
||||||
|
cancelText: t("general.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(
|
||||||
|
{
|
||||||
|
sales_channel_ids: keys.map((k) => ({ id: k })),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setRowSelection({})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const noRecords = !isLoading && !sales_channels?.length && !params.q
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container className="p-0 divide-y">
|
||||||
|
<div className="px-6 py-4 flex items-center justify-between">
|
||||||
|
<Heading level="h2">{t("salesChannels.domain")}</Heading>
|
||||||
|
<Button variant="secondary" size="small" asChild>
|
||||||
|
<Link to="add-sales-channels">{t("general.add")}</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{!noRecords && (
|
||||||
|
<div className="px-6 py-4 flex items-center justify-between">
|
||||||
|
<div></div>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Query />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{noRecords ? (
|
||||||
|
<NoRecords />
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{!isLoading && sales_channels?.length !== 0 ? (
|
||||||
|
<Table>
|
||||||
|
<Table.Header className="border-t-0">
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => {
|
||||||
|
return (
|
||||||
|
<Table.Row
|
||||||
|
key={headerGroup.id}
|
||||||
|
className="[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th:first-of-type]:w-[1%] [&_th:first-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<Table.HeaderCell key={header.id}>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</Table.HeaderCell>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Row>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body className="border-b-0">
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<Table.Row
|
||||||
|
key={row.id}
|
||||||
|
className={clx(
|
||||||
|
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
|
||||||
|
{
|
||||||
|
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
|
||||||
|
row.getIsSelected(),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/settings/sales-channels/${row.original.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<Table.Cell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
))}
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<NoResults />
|
||||||
|
)}
|
||||||
|
<LocalizedTablePagination
|
||||||
|
canNextPage={table.getCanNextPage()}
|
||||||
|
canPreviousPage={table.getCanPreviousPage()}
|
||||||
|
nextPage={table.nextPage}
|
||||||
|
previousPage={table.previousPage}
|
||||||
|
count={count ?? 0}
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
pageCount={table.getPageCount()}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
/>
|
||||||
|
<CommandBar open={!!Object.keys(rowSelection).length}>
|
||||||
|
<CommandBar.Bar>
|
||||||
|
<CommandBar.Value>
|
||||||
|
{t("general.countSelected", {
|
||||||
|
count: Object.keys(rowSelection).length,
|
||||||
|
})}
|
||||||
|
</CommandBar.Value>
|
||||||
|
<CommandBar.Seperator />
|
||||||
|
<CommandBar.Command
|
||||||
|
action={handleRemove}
|
||||||
|
shortcut="r"
|
||||||
|
label={t("general.remove")}
|
||||||
|
/>
|
||||||
|
</CommandBar.Bar>
|
||||||
|
</CommandBar>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SalesChannelActions = ({
|
||||||
|
salesChannel,
|
||||||
|
apiKey,
|
||||||
|
}: {
|
||||||
|
salesChannel: SalesChannel
|
||||||
|
apiKey: string
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const { mutateAsync } = useAdminRemovePublishableKeySalesChannelsBatch(apiKey)
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("apiKeyManagement.removeSalesChannelWarning"),
|
||||||
|
confirmText: t("general.delete"),
|
||||||
|
cancelText: t("general.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
sales_channel_ids: [{ id: salesChannel.id }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
label: t("general.edit"),
|
||||||
|
to: `/settings/sales-channels/${salesChannel.id}/edit`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: <Trash />,
|
||||||
|
label: t("general.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnHelper = createColumnHelper<SalesChannel>()
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("name", {
|
||||||
|
header: t("fields.name"),
|
||||||
|
cell: ({ getValue }) => getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("description", {
|
||||||
|
header: t("fields.description"),
|
||||||
|
cell: ({ getValue }) => getValue(),
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("is_disabled", {
|
||||||
|
header: t("fields.status"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const value = getValue()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StatusBadge color={value ? "grey" : "green"}>
|
||||||
|
{value ? t("general.disabled") : t("general.enabled")}
|
||||||
|
</StatusBadge>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.display({
|
||||||
|
id: "actions",
|
||||||
|
cell: ({ row, table }) => {
|
||||||
|
const { apiKey } = table.options.meta as {
|
||||||
|
apiKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SalesChannelActions salesChannel={row.original} apiKey={apiKey} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./api-key-sales-channel-section"
|
||||||
+1
@@ -1 +1,2 @@
|
|||||||
export { ApiKeyManagementDetail as Component } from "./api-key-management-detail"
|
export { ApiKeyManagementDetail as Component } from "./api-key-management-detail"
|
||||||
|
export { apiKeyLoader as loader } from "./loader"
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
import { AdminPublishableApiKeysRes } 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 apiKeyDetailQuery = (id: string) => ({
|
||||||
|
queryKey: adminProductKeys.detail(id),
|
||||||
|
queryFn: async () => medusa.admin.publishableApiKeys.retrieve(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const apiKeyLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
const id = params.id
|
||||||
|
const query = apiKeyDetailQuery(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<Response<AdminPublishableApiKeysRes>>(
|
||||||
|
query.queryKey
|
||||||
|
) ?? (await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
+31
-3
@@ -1,12 +1,40 @@
|
|||||||
import { Drawer } from "@medusajs/ui"
|
import { Drawer, Heading } from "@medusajs/ui"
|
||||||
|
import { useAdminPublishableApiKey } from "medusa-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useParams } from "react-router-dom"
|
||||||
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
import { useRouteModalState } from "../../../hooks/use-route-modal-state"
|
||||||
|
import { EditApiKeyForm } from "./components/edit-api-key-form"
|
||||||
|
|
||||||
export const ApiKeyManagementEdit = () => {
|
export const ApiKeyManagementEdit = () => {
|
||||||
const [open, onOpenChange] = useRouteModalState()
|
const [open, onOpenChange, subscribe] = useRouteModalState()
|
||||||
|
const { id } = useParams()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { publishable_api_key, isLoading, isError, error } =
|
||||||
|
useAdminPublishableApiKey(id!)
|
||||||
|
|
||||||
|
const handleSuccessfulSubmit = () => {
|
||||||
|
onOpenChange(false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={open} onOpenChange={onOpenChange}>
|
<Drawer open={open} onOpenChange={onOpenChange}>
|
||||||
<Drawer.Content></Drawer.Content>
|
<Drawer.Content>
|
||||||
|
<Drawer.Header>
|
||||||
|
<Heading>{t("apiKeyManagement.editKey")}</Heading>
|
||||||
|
</Drawer.Header>
|
||||||
|
{!isLoading && publishable_api_key && (
|
||||||
|
<EditApiKeyForm
|
||||||
|
apiKey={publishable_api_key}
|
||||||
|
onSuccessfulSubmit={handleSuccessfulSubmit}
|
||||||
|
subscribe={subscribe}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Drawer.Content>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import type { PublishableApiKey } from "@medusajs/medusa"
|
||||||
|
import { Button, Drawer, Input } from "@medusajs/ui"
|
||||||
|
import { useAdminUpdatePublishableApiKey } 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 EditApiKeyFormProps = {
|
||||||
|
apiKey: PublishableApiKey
|
||||||
|
onSuccessfulSubmit: () => void
|
||||||
|
subscribe: (state: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditApiKeySchema = zod.object({
|
||||||
|
title: zod.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EditApiKeyForm = ({
|
||||||
|
apiKey,
|
||||||
|
onSuccessfulSubmit,
|
||||||
|
subscribe,
|
||||||
|
}: EditApiKeyFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const form = useForm<zod.infer<typeof EditApiKeySchema>>({
|
||||||
|
defaultValues: {
|
||||||
|
title: apiKey.title,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(EditApiKeySchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
formState: { isDirty },
|
||||||
|
} = form
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
subscribe(isDirty)
|
||||||
|
}, [isDirty])
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = useAdminUpdatePublishableApiKey(apiKey.id)
|
||||||
|
|
||||||
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
|
await mutateAsync(data, {
|
||||||
|
onSuccess: () => {
|
||||||
|
onSuccessfulSubmit()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||||
|
<Drawer.Body>
|
||||||
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="title"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>{t("fields.title")}</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input {...field} />
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Drawer.Body>
|
||||||
|
<Drawer.Footer>
|
||||||
|
<div className="flex items-center 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./edit-api-key-form"
|
||||||
+53
-4
@@ -1,6 +1,16 @@
|
|||||||
import { PencilSquare, Trash, XCircle } from "@medusajs/icons"
|
import { PencilSquare, Trash, XCircle } from "@medusajs/icons"
|
||||||
import { PublishableApiKey } from "@medusajs/medusa"
|
import { PublishableApiKey } from "@medusajs/medusa"
|
||||||
import { Button, Container, Heading, Table, clx, usePrompt } from "@medusajs/ui"
|
import {
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Copy,
|
||||||
|
Heading,
|
||||||
|
StatusBadge,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
clx,
|
||||||
|
usePrompt,
|
||||||
|
} from "@medusajs/ui"
|
||||||
import {
|
import {
|
||||||
PaginationState,
|
PaginationState,
|
||||||
RowSelectionState,
|
RowSelectionState,
|
||||||
@@ -9,6 +19,7 @@ import {
|
|||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table"
|
} from "@tanstack/react-table"
|
||||||
|
import { format } from "date-fns"
|
||||||
import {
|
import {
|
||||||
useAdminDeletePublishableApiKey,
|
useAdminDeletePublishableApiKey,
|
||||||
useAdminPublishableApiKeys,
|
useAdminPublishableApiKeys,
|
||||||
@@ -71,7 +82,7 @@ export const ApiKeyManagementListTable = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="p-0">
|
<Container className="p-0 divide-y">
|
||||||
<div className="px-6 py-4 flex items-center justify-between">
|
<div className="px-6 py-4 flex items-center justify-between">
|
||||||
<Heading level="h2">{t("apiKeyManagement.domain")}</Heading>
|
<Heading level="h2">{t("apiKeyManagement.domain")}</Heading>
|
||||||
<Link to="create">
|
<Link to="create">
|
||||||
@@ -89,7 +100,7 @@ export const ApiKeyManagementListTable = () => {
|
|||||||
return (
|
return (
|
||||||
<Table.Row
|
<Table.Row
|
||||||
key={headerGroup.id}
|
key={headerGroup.id}
|
||||||
className="[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/3"
|
className="[&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/4"
|
||||||
>
|
>
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
return (
|
return (
|
||||||
@@ -247,7 +258,45 @@ const useColumns = () => {
|
|||||||
}),
|
}),
|
||||||
columnHelper.accessor("id", {
|
columnHelper.accessor("id", {
|
||||||
header: "Key",
|
header: "Key",
|
||||||
cell: ({ getValue }) => getValue(),
|
cell: ({ getValue }) => {
|
||||||
|
const token = getValue()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bg-ui-bg-subtle border border-ui-border-base flex items-center gap-x-0.5 w-fit max-w-[220px] rounded-full pl-2 pr-1 box-border cursor-default overflow-hidden"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Text size="xsmall" leading="compact" className="truncate">
|
||||||
|
{token}
|
||||||
|
</Text>
|
||||||
|
<Copy
|
||||||
|
content={token}
|
||||||
|
variant="mini"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("revoked_at", {
|
||||||
|
header: t("fields.status"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const revokedAt = getValue()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusBadge color={revokedAt ? "red" : "green"}>
|
||||||
|
{revokedAt ? t("general.revoked") : t("general.active")}
|
||||||
|
</StatusBadge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor("created_at", {
|
||||||
|
header: t("fields.created"),
|
||||||
|
cell: ({ getValue }) => {
|
||||||
|
const date = getValue()
|
||||||
|
|
||||||
|
return format(new Date(date), "dd MMM, yyyy")
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: "actions",
|
id: "actions",
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ type LocationSalesChannelSectionProps = {
|
|||||||
location: StockLocationExpandedDTO
|
location: StockLocationExpandedDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
export const LocationSalesChannelSection = ({
|
export const LocationSalesChannelSection = ({
|
||||||
location,
|
location,
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
export { salesChannelLoader as loader } from "./loader"
|
||||||
export { SalesChannelDetail as Component } from "./sales-channel-detail"
|
export { SalesChannelDetail as Component } from "./sales-channel-detail"
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
import { AdminSalesChannelsRes } 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 salesChannelDetailQuery = (id: string) => ({
|
||||||
|
queryKey: adminProductKeys.detail(id),
|
||||||
|
queryFn: async () => medusa.admin.salesChannels.retrieve(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const salesChannelLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
const id = params.id
|
||||||
|
const query = salesChannelDetailQuery(id!)
|
||||||
|
|
||||||
|
return (
|
||||||
|
queryClient.getQueryData<Response<AdminSalesChannelsRes>>(query.queryKey) ??
|
||||||
|
(await queryClient.fetchQuery(query))
|
||||||
|
)
|
||||||
|
}
|
||||||
+9
-2
@@ -1,13 +1,20 @@
|
|||||||
import { useAdminSalesChannel } from "medusa-react"
|
import { useAdminSalesChannel } from "medusa-react"
|
||||||
import { Outlet, useParams } from "react-router-dom"
|
import { Outlet, useLoaderData, useParams } from "react-router-dom"
|
||||||
|
|
||||||
import { JsonViewSection } from "../../../components/common/json-view-section"
|
import { JsonViewSection } from "../../../components/common/json-view-section"
|
||||||
import { SalesChannelGeneralSection } from "./components/sales-channel-general-section"
|
import { SalesChannelGeneralSection } from "./components/sales-channel-general-section"
|
||||||
import { SalesChannelProductSection } from "./components/sales-channel-product-section"
|
import { SalesChannelProductSection } from "./components/sales-channel-product-section"
|
||||||
|
import { salesChannelLoader } from "./loader"
|
||||||
|
|
||||||
export const SalesChannelDetail = () => {
|
export const SalesChannelDetail = () => {
|
||||||
|
const initialData = useLoaderData() as Awaited<
|
||||||
|
ReturnType<typeof salesChannelLoader>
|
||||||
|
>
|
||||||
|
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { sales_channel, isLoading } = useAdminSalesChannel(id!)
|
const { sales_channel, isLoading } = useAdminSalesChannel(id!, {
|
||||||
|
initialData,
|
||||||
|
})
|
||||||
|
|
||||||
if (isLoading || !sales_channel) {
|
if (isLoading || !sales_channel) {
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
|
|||||||
+30
-15
@@ -51,11 +51,16 @@ export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
|
|||||||
{t("store.swapLinkTemplate")}
|
{t("store.swapLinkTemplate")}
|
||||||
</Text>
|
</Text>
|
||||||
{store.swap_link_template ? (
|
{store.swap_link_template ? (
|
||||||
<Copy content={store.swap_link_template} asChild>
|
<div className="bg-ui-bg-subtle border border-ui-border-base flex items-center gap-x-0.5 w-fit rounded-full pl-2 pr-1 box-border cursor-default overflow-hidden">
|
||||||
<Badge className="w-fit cursor-pointer" rounded="full">
|
<Text size="xsmall" leading="compact" className="truncate">
|
||||||
<span>{store.swap_link_template}</span>
|
{store.swap_link_template}
|
||||||
</Badge>
|
</Text>
|
||||||
</Copy>
|
<Copy
|
||||||
|
content={store.swap_link_template}
|
||||||
|
variant="mini"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Text size="small" leading="compact">
|
<Text size="small" leading="compact">
|
||||||
-
|
-
|
||||||
@@ -67,11 +72,16 @@ export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
|
|||||||
{t("store.paymentLinkTemplate")}
|
{t("store.paymentLinkTemplate")}
|
||||||
</Text>
|
</Text>
|
||||||
{store.payment_link_template ? (
|
{store.payment_link_template ? (
|
||||||
<Copy content={store.payment_link_template} asChild>
|
<div className="bg-ui-bg-subtle border border-ui-border-base flex items-center gap-x-0.5 w-fit rounded-full pl-2 pr-1 box-border cursor-default overflow-hidden">
|
||||||
<Badge className="w-fit cursor-pointer" rounded="full">
|
<Text size="xsmall" leading="compact" className="truncate">
|
||||||
<span>{store.payment_link_template}</span>
|
{store.payment_link_template}
|
||||||
</Badge>
|
</Text>
|
||||||
</Copy>
|
<Copy
|
||||||
|
content={store.payment_link_template}
|
||||||
|
variant="mini"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Text size="small" leading="compact">
|
<Text size="small" leading="compact">
|
||||||
-
|
-
|
||||||
@@ -83,11 +93,16 @@ export const StoreGeneralSection = ({ store }: StoreGeneralSectionProps) => {
|
|||||||
{t("store.inviteLinkTemplate")}
|
{t("store.inviteLinkTemplate")}
|
||||||
</Text>
|
</Text>
|
||||||
{store.invite_link_template ? (
|
{store.invite_link_template ? (
|
||||||
<Copy content={store.invite_link_template} asChild>
|
<div className="bg-ui-bg-subtle border border-ui-border-base flex items-center gap-x-0.5 w-fit rounded-full pl-2 pr-1 box-border cursor-default overflow-hidden">
|
||||||
<Badge className="w-fit cursor-pointer" rounded="full">
|
<Text size="xsmall" leading="compact" className="truncate">
|
||||||
<span>{store.invite_link_template}</span>
|
{store.invite_link_template}
|
||||||
</Badge>
|
</Text>
|
||||||
</Copy>
|
<Copy
|
||||||
|
content={store.invite_link_template}
|
||||||
|
variant="mini"
|
||||||
|
className="text-ui-fg-subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Text size="small" leading="compact">
|
<Text size="small" leading="compact">
|
||||||
-
|
-
|
||||||
|
|||||||
@@ -14,18 +14,24 @@ const avatarVariants = cva({
|
|||||||
rounded: "rounded-full",
|
rounded: "rounded-full",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
xsmall: "h-5 w-5",
|
"2xsmall": "h-5 w-5",
|
||||||
small: "h-6 w-6",
|
xsmall: "h-6 w-6",
|
||||||
|
small: "h-7 w-7",
|
||||||
base: "h-8 w-8",
|
base: "h-8 w-8",
|
||||||
large: "h-10 w-10",
|
large: "h-10 w-10",
|
||||||
xlarge: "h-12 w-12",
|
xlarge: "h-12 w-12",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
compoundVariants: [
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
variant: "squared",
|
||||||
|
size: "2xsmall",
|
||||||
|
className: "rounded-md",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
variant: "squared",
|
variant: "squared",
|
||||||
size: "xsmall",
|
size: "xsmall",
|
||||||
className: "rounded-[4px]",
|
className: "rounded-md",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
variant: "squared",
|
variant: "squared",
|
||||||
@@ -62,14 +68,20 @@ const innerVariants = cva({
|
|||||||
rounded: "rounded-full",
|
rounded: "rounded-full",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
xsmall: "txt-compact-xsmall-plus h-4 w-4",
|
"2xsmall": "txt-compact-xsmall-plus h-4 w-4",
|
||||||
small: "txt-compact-xsmall-plus h-5 w-5",
|
xsmall: "txt-compact-xsmall-plus h-5 w-5",
|
||||||
|
small: "txt-compact-small-plus h-6 w-6",
|
||||||
base: "txt-compact-small-plus h-7 w-7",
|
base: "txt-compact-small-plus h-7 w-7",
|
||||||
large: "txt-compact-medium-plus h-9 w-9",
|
large: "txt-compact-medium-plus h-9 w-9",
|
||||||
xlarge: "txt-compact-large-plus h-11 w-11",
|
xlarge: "txt-compact-large-plus h-11 w-11",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
compoundVariants: [
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
variant: "squared",
|
||||||
|
size: "2xsmall",
|
||||||
|
className: "rounded-sm",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
variant: "squared",
|
variant: "squared",
|
||||||
size: "xsmall",
|
size: "xsmall",
|
||||||
|
|||||||
@@ -2,77 +2,115 @@
|
|||||||
|
|
||||||
import { Tooltip } from "@/components/tooltip"
|
import { Tooltip } from "@/components/tooltip"
|
||||||
import { clx } from "@/utils/clx"
|
import { clx } from "@/utils/clx"
|
||||||
import { CheckCircleSolid, SquareTwoStack } from "@medusajs/icons"
|
import {
|
||||||
|
CheckCircleMiniSolid,
|
||||||
|
CheckCircleSolid,
|
||||||
|
SquareTwoStack,
|
||||||
|
SquareTwoStackMini,
|
||||||
|
} from "@medusajs/icons"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import copy from "copy-to-clipboard"
|
import copy from "copy-to-clipboard"
|
||||||
import React, { useState } from "react"
|
import React, { useState } from "react"
|
||||||
|
|
||||||
type CopyProps = React.HTMLAttributes<HTMLButtonElement> & {
|
type CopyProps = React.HTMLAttributes<HTMLButtonElement> & {
|
||||||
content: string
|
content: string
|
||||||
|
variant?: "mini" | "default" | null
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component is based on the `button` element and supports all of its props
|
* This component is based on the `button` element and supports all of its props
|
||||||
*/
|
*/
|
||||||
const Copy = React.forwardRef<
|
const Copy = React.forwardRef<HTMLButtonElement, CopyProps>(
|
||||||
HTMLButtonElement,
|
(
|
||||||
CopyProps
|
{
|
||||||
>(({
|
children,
|
||||||
children,
|
className,
|
||||||
className,
|
/**
|
||||||
/**
|
* The content to copy.
|
||||||
* The content to copy.
|
*/
|
||||||
*/
|
content,
|
||||||
content,
|
/**
|
||||||
/**
|
* The variant of the copy button.
|
||||||
* Whether to remove the wrapper `button` element and use the
|
*/
|
||||||
* passed child element instead.
|
variant = "default",
|
||||||
*/
|
/**
|
||||||
asChild = false,
|
* Whether to remove the wrapper `button` element and use the
|
||||||
...props
|
* passed child element instead.
|
||||||
}: CopyProps, ref) => {
|
*/
|
||||||
const [done, setDone] = useState(false)
|
asChild = false,
|
||||||
const [open, setOpen] = useState(false)
|
...props
|
||||||
const [text, setText] = useState("Copy")
|
}: CopyProps,
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [done, setDone] = useState(false)
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [text, setText] = useState("Copy")
|
||||||
|
|
||||||
const copyToClipboard = () => {
|
const copyToClipboard = (
|
||||||
setDone(true)
|
e:
|
||||||
copy(content)
|
| React.MouseEvent<HTMLElement, MouseEvent>
|
||||||
|
| React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
setTimeout(() => {
|
setDone(true)
|
||||||
setDone(false)
|
copy(content)
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
setTimeout(() => {
|
||||||
if (done) {
|
setDone(false)
|
||||||
setText("Copied")
|
}, 2000)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
React.useEffect(() => {
|
||||||
setText("Copy")
|
if (done) {
|
||||||
}, 500)
|
setText("Copied")
|
||||||
}, [done])
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const Component = asChild ? Slot : "button"
|
setTimeout(() => {
|
||||||
|
setText("Copy")
|
||||||
|
}, 500)
|
||||||
|
}, [done])
|
||||||
|
|
||||||
return (
|
const isDefaultVariant = (
|
||||||
<Tooltip content={text} open={done || open} onOpenChange={setOpen}>
|
variant?: string | null
|
||||||
<Component
|
): variant is "default" => {
|
||||||
ref={ref}
|
return variant === "default"
|
||||||
aria-label="Copy code snippet"
|
}
|
||||||
type="button"
|
|
||||||
className={clx("text-ui-code-icon h-fit w-fit", className)}
|
const isDefault = isDefaultVariant(variant)
|
||||||
onClick={copyToClipboard}
|
|
||||||
{...props}
|
const Component = asChild ? Slot : "button"
|
||||||
>
|
|
||||||
{children ? children : done ? <CheckCircleSolid /> : <SquareTwoStack />}
|
return (
|
||||||
</Component>
|
<Tooltip content={text} open={done || open} onOpenChange={setOpen}>
|
||||||
</Tooltip>
|
<Component
|
||||||
)
|
ref={ref}
|
||||||
})
|
aria-label="Copy code snippet"
|
||||||
|
type="button"
|
||||||
|
className={clx("text-ui-code-icon h-fit w-fit", className)}
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ? (
|
||||||
|
children
|
||||||
|
) : done ? (
|
||||||
|
isDefault ? (
|
||||||
|
<CheckCircleSolid />
|
||||||
|
) : (
|
||||||
|
<CheckCircleMiniSolid />
|
||||||
|
)
|
||||||
|
) : isDefault ? (
|
||||||
|
<SquareTwoStack />
|
||||||
|
) : (
|
||||||
|
<SquareTwoStackMini />
|
||||||
|
)}
|
||||||
|
</Component>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
Copy.displayName = "Copy"
|
Copy.displayName = "Copy"
|
||||||
|
|
||||||
export { Copy }
|
export { Copy }
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { clx } from "@/utils/clx"
|
import { clx } from "@/utils/clx"
|
||||||
|
import { VariantProps, cva } from "cva"
|
||||||
|
|
||||||
|
const statusBadgeVariants = cva({
|
||||||
|
base: "flex items-center justify-center w-5 h-[18px] [&_div]:w-2 [&_div]:h-2 [&_div]:rounded-sm",
|
||||||
|
variants: {
|
||||||
|
color: {
|
||||||
|
green: "[&_div]:bg-ui-tag-green-icon",
|
||||||
|
red: "[&_div]:bg-ui-tag-red-icon",
|
||||||
|
orange: "[&_div]:bg-ui-tag-orange-icon",
|
||||||
|
blue: "[&_div]:bg-ui-tag-blue-icon",
|
||||||
|
purple: "[&_div]:bg-ui-tag-purple-icon",
|
||||||
|
grey: "[&_div]:bg-ui-tag-neutral-icon",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
color: "grey",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
interface StatusBadgeProps
|
interface StatusBadgeProps
|
||||||
extends Omit<React.ComponentPropsWithoutRef<"span">, "color"> {
|
extends Omit<React.ComponentPropsWithoutRef<"span">, "color">,
|
||||||
color?: "green" | "red" | "blue" | "orange" | "grey" | "purple"
|
VariantProps<typeof statusBadgeVariants> {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component is based on the span element and supports all of its props
|
* This component is based on the span element and supports all of its props
|
||||||
@@ -27,31 +44,14 @@ const StatusBadge = React.forwardRef<HTMLSpanElement, StatusBadgeProps>(
|
|||||||
<span
|
<span
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clx(
|
className={clx(
|
||||||
"bg-ui-bg-base border-ui-border-base txt-compact-small text-ui-fg-base inline-flex items-center justify-center rounded-full border py-[3px] pl-1 pr-2.5",
|
"txt-compact-xsmall-plus bg-ui-bg-subtle text-ui-fg-subtle border-ui-border-base box-border flex w-fit select-none items-center overflow-hidden rounded-md border pl-0 pr-1 leading-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span
|
<div role="presentation" className={statusBadgeVariants({ color })}>
|
||||||
role="presentation"
|
<div />
|
||||||
className="mr-0.5 flex h-5 w-5 items-center justify-center"
|
</div>
|
||||||
>
|
|
||||||
<span
|
|
||||||
role="presentation"
|
|
||||||
className="bg-ui-bg-base shadow-borders-base flex h-2.5 w-2.5 items-center justify-center rounded-full"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={clx("h-1.5 w-1.5 rounded-full", {
|
|
||||||
"bg-ui-tag-neutral-icon": color === "grey",
|
|
||||||
"bg-ui-tag-green-icon": color === "green",
|
|
||||||
"bg-ui-tag-red-icon": color === "red",
|
|
||||||
"bg-ui-tag-orange-icon": color === "orange",
|
|
||||||
"bg-ui-tag-blue-icon": color === "blue",
|
|
||||||
"bg-ui-tag-purple-icon": color === "purple",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user