feat: Bring back API key sales channel management (#6976)

- Add API key sales channel management
- Add HTTP responses for API keys and sales channels
- Use HTTP responses in `dashboard` and remove now redundant types
This commit is contained in:
Oli Juhl
2024-04-06 18:30:27 +02:00
committed by GitHub
parent 58c68f6715
commit 5724d80286
22 changed files with 533 additions and 607 deletions

View File

@@ -10,11 +10,9 @@ import { client } from "../../lib/client"
import { queryClient } from "../../lib/medusa"
import { queryKeysFactory } from "../../lib/query-key-factory"
import { CreateApiKeyReq, UpdateApiKeyReq } from "../../types/api-payloads"
import {
ApiKeyDeleteRes,
ApiKeyListRes,
ApiKeyRes,
} from "../../types/api-responses"
import { ApiKeyDeleteRes } from "../../types/api-responses"
import { AdminApiKeyResponse, AdminApiKeyListResponse } from "@medusajs/types"
import { salesChannelsQueryKeys } from "./sales-channels"
const API_KEYS_QUERY_KEY = "api_keys" as const
export const apiKeysQueryKeys = queryKeysFactory(API_KEYS_QUERY_KEY)
@@ -23,7 +21,7 @@ export const useApiKey = (
id: string,
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<ApiKeyRes, Error, ApiKeyRes, QueryKey>,
UseQueryOptions<AdminApiKeyResponse, Error, AdminApiKeyResponse, QueryKey>,
"queryKey" | "queryFn"
>
) => {
@@ -39,7 +37,12 @@ export const useApiKey = (
export const useApiKeys = (
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<ApiKeyListRes, Error, ApiKeyListRes, QueryKey>,
UseQueryOptions<
AdminApiKeyListResponse,
Error,
AdminApiKeyListResponse,
QueryKey
>,
"queryKey" | "queryFn"
>
) => {
@@ -53,7 +56,7 @@ export const useApiKeys = (
}
export const useCreateApiKey = (
options?: UseMutationOptions<ApiKeyRes, Error, CreateApiKeyReq>
options?: UseMutationOptions<AdminApiKeyResponse, Error, CreateApiKeyReq>
) => {
return useMutation({
mutationFn: (payload) => client.apiKeys.create(payload),
@@ -68,7 +71,7 @@ export const useCreateApiKey = (
export const useUpdateApiKey = (
id: string,
options?: UseMutationOptions<ApiKeyRes, Error, UpdateApiKeyReq>
options?: UseMutationOptions<AdminApiKeyResponse, Error, UpdateApiKeyReq>
) => {
return useMutation({
mutationFn: (payload) => client.apiKeys.update(id, payload),
@@ -84,7 +87,7 @@ export const useUpdateApiKey = (
export const useRevokeApiKey = (
id: string,
options?: UseMutationOptions<ApiKeyRes, Error, void>
options?: UseMutationOptions<AdminApiKeyResponse, Error, void>
) => {
return useMutation({
mutationFn: () => client.apiKeys.revoke(id),
@@ -111,3 +114,50 @@ export const useDeleteApiKey = (
},
})
}
export const useBatchRemoveSalesChannelsFromApiKey = (
id: string,
options?: UseMutationOptions<
AdminApiKeyResponse,
Error,
{ sales_channel_ids: string[] }
>
) => {
return useMutation({
mutationFn: (payload) =>
client.apiKeys.batchRemoveSalesChannels(id, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: apiKeysQueryKeys.lists() })
queryClient.invalidateQueries({ queryKey: apiKeysQueryKeys.detail(id) })
queryClient.invalidateQueries({
queryKey: salesChannelsQueryKeys.lists(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}
export const useBatchAddSalesChannelsToApiKey = (
id: string,
options?: UseMutationOptions<
AdminApiKeyResponse,
Error,
{ sales_channel_ids: string[] }
>
) => {
return useMutation({
mutationFn: (payload) => client.apiKeys.batchAddSalesChannels(id, payload),
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryKey: apiKeysQueryKeys.lists() })
queryClient.invalidateQueries({ queryKey: apiKeysQueryKeys.detail(id) })
queryClient.invalidateQueries({
queryKey: salesChannelsQueryKeys.lists(),
})
options?.onSuccess?.(data, variables, context)
},
...options,
})
}

View File

@@ -15,20 +15,25 @@ import {
RemoveProductsSalesChannelReq,
UpdateSalesChannelReq,
} from "../../types/api-payloads"
import {
SalesChannelDeleteRes,
SalesChannelListRes,
SalesChannelRes,
} from "../../types/api-responses"
import { SalesChannelDeleteRes } from "../../types/api-responses"
import { productsQueryKeys } from "./products"
import {
AdminSalesChannelListResponse,
AdminSalesChannelResponse,
} from "@medusajs/types"
const SALES_CHANNELS_QUERY_KEY = "sales-channels" as const
const salesChannelsQueryKeys = queryKeysFactory(SALES_CHANNELS_QUERY_KEY)
export const salesChannelsQueryKeys = queryKeysFactory(SALES_CHANNELS_QUERY_KEY)
export const useSalesChannel = (
id: string,
options?: Omit<
UseQueryOptions<SalesChannelRes, Error, SalesChannelRes, QueryKey>,
UseQueryOptions<
AdminSalesChannelResponse,
Error,
AdminSalesChannelResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
@@ -44,7 +49,12 @@ export const useSalesChannel = (
export const useSalesChannels = (
query?: Record<string, any>,
options?: Omit<
UseQueryOptions<SalesChannelListRes, Error, SalesChannelListRes, QueryKey>,
UseQueryOptions<
AdminSalesChannelListResponse,
Error,
AdminSalesChannelListResponse,
QueryKey
>,
"queryFn" | "queryKey"
>
) => {
@@ -58,7 +68,11 @@ export const useSalesChannels = (
}
export const useCreateSalesChannel = (
options?: UseMutationOptions<SalesChannelRes, Error, CreateSalesChannelReq>
options?: UseMutationOptions<
AdminSalesChannelResponse,
Error,
CreateSalesChannelReq
>
) => {
return useMutation({
mutationFn: (payload) => client.salesChannels.create(payload),
@@ -74,7 +88,11 @@ export const useCreateSalesChannel = (
export const useUpdateSalesChannel = (
id: string,
options?: UseMutationOptions<SalesChannelRes, Error, UpdateSalesChannelReq>
options?: UseMutationOptions<
AdminSalesChannelResponse,
Error,
UpdateSalesChannelReq
>
) => {
return useMutation({
mutationFn: (payload) => client.salesChannels.update(id, payload),
@@ -120,7 +138,7 @@ export const useDeleteSalesChannel = (
export const useSalesChannelRemoveProducts = (
id: string,
options?: UseMutationOptions<
SalesChannelRes,
AdminSalesChannelResponse,
Error,
RemoveProductsSalesChannelReq
>
@@ -156,7 +174,7 @@ export const useSalesChannelRemoveProducts = (
export const useSalesChannelAddProducts = (
id: string,
options?: UseMutationOptions<
SalesChannelRes,
AdminSalesChannelResponse,
Error,
AddProductsSalesChannelReq
>

View File

@@ -1,17 +1,14 @@
import { AdminApiKeyListResponse, AdminApiKeyResponse } from "@medusajs/types"
import { CreateApiKeyReq, UpdateApiKeyReq } from "../../types/api-payloads"
import {
ApiKeyDeleteRes,
ApiKeyListRes,
ApiKeyRes,
} from "../../types/api-responses"
import { ApiKeyDeleteRes } from "../../types/api-responses"
import { deleteRequest, getRequest, postRequest } from "./common"
const retrieveApiKey = async (id: string, query?: Record<string, any>) => {
return getRequest<ApiKeyRes>(`/admin/api-keys/${id}`, query)
return getRequest<AdminApiKeyResponse>(`/admin/api-keys/${id}`, query)
}
const listApiKeys = async (query?: Record<string, any>) => {
return getRequest<ApiKeyListRes>(`/admin/api-keys`, query)
return getRequest<AdminApiKeyListResponse>(`/admin/api-keys`, query)
}
const deleteApiKey = async (id: string) => {
@@ -19,15 +16,35 @@ const deleteApiKey = async (id: string) => {
}
const revokeApiKey = async (id: string) => {
return postRequest<ApiKeyRes>(`/admin/api-keys/${id}/revoke`)
return postRequest<AdminApiKeyResponse>(`/admin/api-keys/${id}/revoke`)
}
const createApiKey = async (payload: CreateApiKeyReq) => {
return postRequest<ApiKeyRes>(`/admin/api-keys`, payload)
return postRequest<AdminApiKeyResponse>(`/admin/api-keys`, payload)
}
const updateApiKey = async (id: string, payload: UpdateApiKeyReq) => {
return postRequest<ApiKeyRes>(`/admin/api-keys/${id}`, payload)
return postRequest<AdminApiKeyResponse>(`/admin/api-keys/${id}`, payload)
}
const batchRemoveSalesChannelsFromApiKey = async (
id: string,
payload: { sales_channel_ids: string[] }
) => {
return postRequest<AdminApiKeyResponse>(
`/admin/api-keys/${id}/sales-channels/batch/remove`,
payload
)
}
const batchAddSalesChannelsFromApiKey = async (
id: string,
payload: { sales_channel_ids: string[] }
) => {
return postRequest<AdminApiKeyResponse>(
`/admin/api-keys/${id}/sales-channels/batch/add`,
payload
)
}
export const apiKeys = {
@@ -37,4 +54,6 @@ export const apiKeys = {
create: createApiKey,
update: updateApiKey,
revoke: revokeApiKey,
batchRemoveSalesChannels: batchRemoveSalesChannelsFromApiKey,
batchAddSalesChannels: batchAddSalesChannelsFromApiKey,
}

View File

@@ -1,36 +1,42 @@
import {
AdminSalesChannelListResponse,
AdminSalesChannelResponse,
} from "@medusajs/types"
import {
AddProductsSalesChannelReq,
CreateSalesChannelReq,
RemoveProductsSalesChannelReq,
UpdateSalesChannelReq,
} from "../../types/api-payloads"
import {
SalesChannelDeleteRes,
SalesChannelListRes,
SalesChannelRes,
} from "../../types/api-responses"
import { SalesChannelDeleteRes } from "../../types/api-responses"
import { deleteRequest, getRequest, postRequest } from "./common"
async function retrieveSalesChannel(id: string, query?: Record<string, any>) {
return getRequest<SalesChannelRes, Record<string, any>>(
return getRequest<AdminSalesChannelResponse, Record<string, any>>(
`/admin/sales-channels/${id}`,
query
)
}
async function listSalesChannels(query?: Record<string, any>) {
return getRequest<SalesChannelListRes, Record<string, any>>(
return getRequest<AdminSalesChannelListResponse, Record<string, any>>(
`/admin/sales-channels`,
query
)
}
async function createSalesChannel(payload: CreateSalesChannelReq) {
return postRequest<SalesChannelRes>(`/admin/sales-channels`, payload)
return postRequest<AdminSalesChannelResponse>(
`/admin/sales-channels`,
payload
)
}
async function updateSalesChannel(id: string, payload: UpdateSalesChannelReq) {
return postRequest<SalesChannelRes>(`/admin/sales-channels/${id}`, payload)
return postRequest<AdminSalesChannelResponse>(
`/admin/sales-channels/${id}`,
payload
)
}
async function deleteSalesChannel(id: string) {
@@ -41,7 +47,7 @@ async function batchRemoveProducts(
id: string,
payload: RemoveProductsSalesChannelReq
) {
return postRequest<SalesChannelRes>(
return postRequest<AdminSalesChannelResponse>(
`/admin/sales-channels/${id}/products/batch/remove`,
payload
)
@@ -51,7 +57,7 @@ async function batchAddProducts(
id: string,
payload: AddProductsSalesChannelReq
) {
return postRequest<SalesChannelRes>(
return postRequest<AdminSalesChannelResponse>(
`/admin/sales-channels/${id}/products/batch/add`,
payload
)

View File

@@ -7,7 +7,7 @@ import { ErrorBoundary } from "../../components/error/error-boundary"
import { MainLayout } from "../../components/layout-v2/main-layout"
import { SettingsLayout } from "../../components/layout/settings-layout"
import { useMe } from "../../hooks/api/users"
import { ApiKeyRes } from "../../types/api-responses"
import { AdminApiKeyResponse } from "@medusajs/types"
import { SearchProvider } from "../search-provider"
import { SidebarProvider } from "../sidebar-provider"
@@ -489,8 +489,7 @@ export const v2Routes: RouteObject[] = [
"../../v2-routes/api-key-management/api-key-management-detail"
),
handle: {
crumb: (data: ApiKeyRes) => {
console.log("data", data)
crumb: (data: AdminApiKeyResponse) => {
return data.api_key.title
},
},

View File

@@ -3,7 +3,6 @@
*/
import {
ApiKeyDTO,
CampaignDTO,
CurrencyDTO,
CustomerDTO,
@@ -22,8 +21,8 @@ import {
StoreDTO,
UserDTO,
} from "@medusajs/types"
import { WorkflowExecutionDTO } from "../v2-routes/workflow-executions/types"
import { ProductTagDTO } from "@medusajs/types/dist/product"
import { WorkflowExecutionDTO } from "../v2-routes/workflow-executions/types"
type ListRes = {
count: number
@@ -77,18 +76,9 @@ export type CampaignListRes = { campaigns: CampaignDTO[] } & ListRes
export type CampaignDeleteRes = DeleteRes
// API Keys
export type ExtendedApiKeyDTO = ApiKeyDTO & {
sales_channels: SalesChannelDTO[] | null
}
export type ApiKeyRes = { api_key: ExtendedApiKeyDTO }
export type ApiKeyListRes = { api_keys: ExtendedApiKeyDTO[] } & ListRes
export type ApiKeyDeleteRes = DeleteRes
// Sales Channels
export type SalesChannelRes = { sales_channel: SalesChannelDTO }
export type SalesChannelListRes = {
sales_channels: SalesChannelDTO[]
} & ListRes
export type SalesChannelDeleteRes = DeleteRes
// Currencies

View File

@@ -1,37 +1,27 @@
// Missing Sales Channel endpoint for API Key domain
import { useParams } from "react-router-dom"
import { RouteFocusModal } from "../../../components/route-modal"
import { useSalesChannels } from "../../../hooks/api/sales-channels"
import { AddSalesChannelsToApiKeyForm } from "./components"
export const ApiKeyManagementAddSalesChannels = () => {
const { id } = useParams()
// const { data, isLoading, isError, error } = useAdminCustomQuery(
// `/api-keys/${id}`,
// [adminPublishableApiKeysKeys.detailSalesChannels(id!)],
// {
// fields: "id,*sales_channels",
// },
// {
// keepPreviousData: true,
// }
// )
const { sales_channels, isLoading, isError, error } = useSalesChannels({
publishable_key_id: id,
})
// if (isError) {
// throw error
// }
// const salesChannels = data?.api_key?.sales_channels
if (isError) {
throw error
}
return (
<RouteFocusModal>
{/* {!isLoading && salesChannels && (
{!isLoading && !!sales_channels && (
<AddSalesChannelsToApiKeyForm
apiKey={id!}
preSelected={salesChannels.map((sc) => sc.id)}
preSelected={sales_channels.map((sc) => sc.id)}
/>
)} */}
TODO
)}
</RouteFocusModal>
)
}

View File

@@ -1,40 +1,25 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { SalesChannel } from "@medusajs/medusa"
import { Button, Checkbox, Hint, StatusBadge, Tooltip } from "@medusajs/ui"
import {
Button,
Checkbox,
Hint,
StatusBadge,
Table,
Tooltip,
clx,
} from "@medusajs/ui"
import {
PaginationState,
OnChangeFn,
RowSelectionState,
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import {
adminPublishableApiKeysKeys,
useAdminAddPublishableKeySalesChannelsBatch,
useAdminCustomPost,
useAdminSalesChannels,
} from "medusa-react"
import { useEffect, useMemo, useState } from "react"
import { useMemo, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
import { OrderBy } from "../../../../components/filtering/order-by"
import { Query } from "../../../../components/filtering/query"
import { LocalizedTablePagination } from "../../../../components/localization/localized-table-pagination"
import {
RouteFocusModal,
useRouteModal,
} from "../../../../components/route-modal"
import { useQueryParams } from "../../../../hooks/use-query-params"
import { useBatchAddSalesChannelsToApiKey } from "../../../../hooks/api/api-keys"
import { useSalesChannelTableQuery } from "../../../../hooks/table/query/use-sales-channel-table-query"
import { useDataTable } from "../../../../hooks/use-data-table"
import { useSalesChannels } from "../../../../hooks/api/sales-channels"
import { keepPreviousData } from "@tanstack/react-query"
import { DataTable } from "../../../../components/table/data-table"
import { AdminSalesChannelResponse } from "@medusajs/types"
type AddSalesChannelsToApiKeyFormProps = {
apiKey: string
@@ -63,69 +48,51 @@ export const AddSalesChannelsToApiKeyForm = ({
const { setValue } = form
const { mutateAsync, isLoading: isMutating } = useAdminCustomPost(
`/api-keys/${apiKey}/sales-channels/batch/add`,
[adminPublishableApiKeysKeys.detailSalesChannels(apiKey)]
)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0,
// @ts-ignore
const { mutateAsync, isLoading: isMutating } =
useBatchAddSalesChannelsToApiKey(apiKey)
const { raw, searchParams } = useSalesChannelTableQuery({
pageSize: PAGE_SIZE,
})
const pagination = useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize]
)
const columns = useColumns()
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
useEffect(() => {
setValue(
"sales_channel_ids",
Object.keys(rowSelection).filter((k) => rowSelection[k]),
{
shouldDirty: true,
shouldTouch: true,
}
)
}, [rowSelection, setValue])
const params = useQueryParams(["q", "order"])
const { sales_channels, count } = useAdminSalesChannels(
const { sales_channels, count, isLoading } = useSalesChannels(
{ ...searchParams },
{
limit: PAGE_SIZE,
offset: PAGE_SIZE * pageIndex,
...params,
},
{
keepPreviousData: true,
placeholderData: keepPreviousData,
}
)
const columns = useColumns()
const updater: OnChangeFn<RowSelectionState> = (fn) => {
const state = typeof fn === "function" ? fn(rowSelection) : fn
const table = useReactTable({
const ids = Object.keys(state)
setValue("sales_channel_ids", ids, {
shouldDirty: true,
shouldTouch: true,
})
setRowSelection(state)
}
const { table } = useDataTable({
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,
count,
enablePagination: true,
enableRowSelection: (row) => {
return !preSelected.includes(row.id)
return !preSelected.includes(row.original.id!)
},
meta: {
preSelected,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
rowSelection: {
state: rowSelection,
updater,
},
})
@@ -165,85 +132,26 @@ export const AddSalesChannelsToApiKeyForm = ({
</Button>
</div>
</RouteFocusModal.Header>
<RouteFocusModal.Body className="flex h-full w-full flex-col items-center divide-y overflow-y-auto">
<div className="flex w-full items-center justify-between 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>
<RouteFocusModal.Body>
<DataTable
table={table}
columns={columns}
count={count}
pageSize={PAGE_SIZE}
pagination
search
isLoading={isLoading}
queryObject={raw}
orderBy={["name", "created_at", "updated_at"]}
/>
</RouteFocusModal.Body>
</form>
</RouteFocusModal.Form>
)
}
const columnHelper = createColumnHelper<SalesChannel>()
const columnHelper =
createColumnHelper<AdminSalesChannelResponse["sales_channel"]>()
const useColumns = () => {
const { t } = useTranslation()
@@ -266,28 +174,24 @@ const useColumns = () => {
/>
)
},
cell: ({ row, table }) => {
const { preSelected } = table.options.meta as {
preSelected: string[]
}
const isAdded = preSelected.includes(row.original.id)
const isSelected = row.getIsSelected() || isAdded
cell: ({ row }) => {
const isPreSelected = !row.getCanSelect()
const isSelected = row.getIsSelected() || isPreSelected
const Component = (
<Checkbox
checked={isSelected}
disabled={isAdded}
disabled={isPreSelected}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(e) => {
e.stopPropagation()
}}
/>
)
if (isAdded) {
if (isPreSelected) {
return (
<Tooltip
content={t("salesChannels.productAlreadyAdded")}
side="right"
>
<Tooltip content={t("store.currencyAlreadyAdded")} side="right">
{Component}
</Tooltip>
)

View File

@@ -3,6 +3,7 @@ import { JsonViewSection } from "../../../components/common/json-view-section"
import { useApiKey } from "../../../hooks/api/api-keys"
import { ApiKeyGeneralSection } from "./components/api-key-general-section"
import { apiKeyLoader } from "./loader"
import { ApiKeySalesChannelSection } from "./components/api-key-sales-channel-section"
export const ApiKeyManagementDetail = () => {
const initialData = useLoaderData() as Awaited<
@@ -25,7 +26,7 @@ export const ApiKeyManagementDetail = () => {
return (
<div className="flex flex-col gap-y-2">
<ApiKeyGeneralSection apiKey={api_key} />
{/* <ApiKeySalesChannelSection apiKey={apiKey} /> */}
<ApiKeySalesChannelSection apiKey={api_key} />
<JsonViewSection data={api_key} />
<Outlet />
</div>

View File

@@ -1,366 +1,254 @@
// Api Key Sales Channel Endpoint is not part of v2
import { PencilSquare, Trash } from "@medusajs/icons"
import {
Button,
Checkbox,
Container,
Heading,
StatusBadge,
usePrompt,
} from "@medusajs/ui"
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
import {
adminPublishableApiKeysKeys,
useAdminCustomPost,
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 { useSalesChannels } from "../../../../../hooks/api/sales-channels"
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { DataTable } from "../../../../../components/table/data-table"
import { keepPreviousData } from "@tanstack/react-query"
import { AdminApiKeyResponse, AdminSalesChannelResponse } from "@medusajs/types"
import { useBatchRemoveSalesChannelsFromApiKey } from "../../../../../hooks/api/api-keys"
// import { PencilSquare, Trash } from "@medusajs/icons"
// import { 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 {
// adminPublishableApiKeysKeys,
// useAdminCustomPost,
// 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"
// import { ExtendedApiKeyDTO } from "../../../../../types/api-responses"
// import { SalesChannelDTO } from "@medusajs/types"
type ApiKeySalesChannelSectionProps = {
apiKey: AdminApiKeyResponse["api_key"]
}
// type ApiKeySalesChannelSectionProps = {
// apiKey: ExtendedApiKeyDTO
// }
const PAGE_SIZE = 10
// const PAGE_SIZE = 10
export const ApiKeySalesChannelSection = ({
apiKey,
}: ApiKeySalesChannelSectionProps) => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const { t } = useTranslation()
const prompt = usePrompt()
// export const ApiKeySalesChannelSection = ({
// apiKey,
// }: ApiKeySalesChannelSectionProps) => {
// const { t } = useTranslation()
// const navigate = useNavigate()
// const prompt = usePrompt()
const { raw, searchParams } = useSalesChannelTableQuery({
pageSize: PAGE_SIZE,
})
// const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
// pageIndex: 0,
// pageSize: PAGE_SIZE,
// })
const { sales_channels, count, isLoading } = useSalesChannels(
{ ...searchParams, publishable_key_id: apiKey.id },
{
placeholderData: keepPreviousData,
}
)
// const pagination = useMemo(
// () => ({
// pageIndex,
// pageSize,
// }),
// [pageIndex, pageSize]
// )
const columns = useColumns({ apiKey: apiKey.id })
// const filters = useProductTableFilters(["sales_channel_id"])
// const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const { table } = useDataTable({
data: sales_channels ?? [],
columns,
count,
enablePagination: true,
enableRowSelection: true,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
rowSelection: {
state: rowSelection,
updater: setRowSelection,
},
})
// const params = useQueryParams(["q"])
const { mutateAsync } = useBatchRemoveSalesChannelsFromApiKey(apiKey.id)
// const query = {
// ...params,
// fields: "id,*sales_channels",
// }
const handleRemove = async () => {
const keys = Object.keys(rowSelection)
// const salesChannels = apiKey?.sales_channels
// const count = salesChannels?.length || 0
const res = await prompt({
title: t("general.areYouSure"),
description: t("apiKeyManagement.removeSalesChannelsWarning", {
count: keys.length,
}),
confirmText: t("actions.continue"),
cancelText: t("actions.cancel"),
})
// const columns = useColumns()
if (!res) {
return
}
// const table = useReactTable({
// data: salesChannels ?? [],
// 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,
// },
// })
await mutateAsync(
{
sales_channel_ids: keys,
},
{
onSuccess: () => {
setRowSelection({})
},
}
)
}
// const { mutateAsync } = useAdminCustomPost(
// `/api-keys/${apiKey.id}/sales-channels/batch/remove`,
// [adminPublishableApiKeysKeys.detailSalesChannels(apiKey.id)]
// )
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading level="h2">{t("salesChannels.domain")}</Heading>
<Button variant="secondary" size="small" asChild>
<Link to="add-sales-channels">{t("general.add")}</Link>
</Button>
</div>
<DataTable
table={table}
columns={columns}
count={count}
pageSize={PAGE_SIZE}
pagination
search
isLoading={isLoading}
queryObject={raw}
orderBy={["name", "created_at", "updated_at"]}
commands={[
{
action: handleRemove,
label: t("actions.remove"),
shortcut: "r",
},
]}
/>
</Container>
)
}
// const handleRemove = async () => {
// const keys = Object.keys(rowSelection).filter((k) => rowSelection[k])
const SalesChannelActions = ({
salesChannel,
apiKey,
}: {
salesChannel: AdminSalesChannelResponse["sales_channel"]
apiKey: string
}) => {
const { t } = useTranslation()
const prompt = usePrompt()
// const res = await prompt({
// title: t("general.areYouSure"),
// description: t("apiKeyManagement.removeSalesChannelsWarning", {
// count: keys.length,
// }),
// confirmText: t("actions.continue"),
// cancelText: t("actions.cancel"),
// })
const { mutateAsync } = useBatchRemoveSalesChannelsFromApiKey(apiKey)
// if (!res) {
// return
// }
const handleDelete = async () => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("apiKeyManagement.removeSalesChannelWarning", {
name: salesChannel.name,
}),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
// await mutateAsync(
// {
// sales_channel_ids: keys,
// },
// {
// onSuccess: () => {
// setRowSelection({})
// },
// }
// )
// }
if (!res) {
return
}
// const noRecords = !salesChannels?.length && !params.q
await mutateAsync({
sales_channel_ids: [salesChannel.id],
})
}
// return (
// <Container className="divide-y p-0">
// <div className="flex items-center justify-between px-6 py-4">
// <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="flex items-center justify-between px-6 py-4">
// <div></div>
// <div className="flex items-center gap-x-2">
// <Query />
// </div>
// </div>
// )}
// {noRecords ? (
// <NoRecords />
// ) : (
// <div>
// {salesChannels?.length !== 0 ? (
// <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 [&_th:last-of-type]:w-[1%] [&_th:last-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("actions.remove")}
// />
// </CommandBar.Bar>
// </CommandBar>
// </div>
// )}
// </Container>
// )
// }
return (
<ActionMenu
groups={[
{
actions: [
{
icon: <PencilSquare />,
label: t("actions.edit"),
to: `/settings/sales-channels/${salesChannel.id}/edit`,
},
],
},
{
actions: [
{
icon: <Trash />,
label: t("actions.delete"),
onClick: handleDelete,
},
],
},
]}
/>
)
}
// const SalesChannelActions = ({
// salesChannel,
// apiKey,
// }: {
// salesChannel: SalesChannelDTO
// apiKey: string
// }) => {
// const { t } = useTranslation()
// const prompt = usePrompt()
const columnHelper =
createColumnHelper<AdminSalesChannelResponse["sales_channel"]>()
// const { mutateAsync } = useAdminRemovePublishableKeySalesChannelsBatch(apiKey)
const useColumns = ({ apiKey }: { apiKey: string }) => {
const { t } = useTranslation()
// const handleDelete = async () => {
// const res = await prompt({
// title: t("general.areYouSure"),
// description: t("apiKeyManagement.removeSalesChannelWarning"),
// confirmText: t("actions.delete"),
// cancelText: t("actions.cancel"),
// })
// if (!res) {
// return
// }
// await mutateAsync({
// sales_channel_ids: [{ id: salesChannel.id }],
// })
// }
// return (
// <ActionMenu
// groups={[
// {
// actions: [
// {
// icon: <PencilSquare />,
// label: t("actions.edit"),
// to: `/settings/sales-channels/${salesChannel.id}/edit`,
// },
// ],
// },
// {
// actions: [
// {
// icon: <Trash />,
// label: t("actions.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]
// )
// }
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 }) => {
return (
<SalesChannelActions salesChannel={row.original} apiKey={apiKey} />
)
},
}),
],
[t]
)
}

View File

@@ -1,9 +1,9 @@
import { LoaderFunctionArgs } from "react-router-dom"
import { AdminApiKeyResponse } from "@medusajs/types"
import { apiKeysQueryKeys } from "../../../hooks/api/api-keys"
import { client } from "../../../lib/client"
import { queryClient } from "../../../lib/medusa"
import { ApiKeyRes } from "../../../types/api-responses"
const apiKeyDetailQuery = (id: string) => ({
queryKey: apiKeysQueryKeys.detail(id),
@@ -15,7 +15,7 @@ export const apiKeyLoader = async ({ params }: LoaderFunctionArgs) => {
const query = apiKeyDetailQuery(id!)
return (
queryClient.getQueryData<ApiKeyRes>(query.queryKey) ??
queryClient.getQueryData<AdminApiKeyResponse>(query.queryKey) ??
(await queryClient.fetchQuery(query))
)
}

View File

@@ -9,13 +9,16 @@ import {
} from "medusa-react"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { ApiKeyDTO } from "@medusajs/types"
import { AdminApiKeyResponse } from "@medusajs/types"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { DateCell } from "../../../../../components/table/table-cells/common/date-cell"
import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
import {
useDeleteApiKey,
useRevokeApiKey,
} from "../../../../../hooks/api/api-keys"
const columnHelper = createColumnHelper<ApiKeyDTO>()
const columnHelper = createColumnHelper<AdminApiKeyResponse["api_key"]>()
export const useApiKeyManagementTableColumns = () => {
const { t } = useTranslation()
@@ -83,21 +86,9 @@ export const useApiKeyManagementTableColumns = () => {
)
}
const ApiKeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
const { mutateAsync: revokeAsync } = useAdminCustomPost(
`/api-keys/${apiKey.id}/revoke`,
[
adminPublishableApiKeysKeys.lists(),
adminPublishableApiKeysKeys.detail(apiKey.id),
]
)
const { mutateAsync: deleteAsync } = useAdminCustomDelete(
`/api-keys/${apiKey.id}`,
[
adminPublishableApiKeysKeys.lists(),
adminPublishableApiKeysKeys.detail(apiKey.id),
]
)
export const ApiKeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
const { mutateAsync: revokeAsync } = useRevokeApiKey(apiKey.id)
const { mutateAsync: deleteAsync } = useDeleteApiKey(apiKey.id)
const { t } = useTranslation()
const prompt = usePrompt()
@@ -133,7 +124,7 @@ const ApiKeyActions = ({ apiKey }: { apiKey: PublishableApiKey }) => {
return
}
await revokeAsync({})
await revokeAsync(undefined)
}
return (

View File

@@ -1,9 +1,9 @@
import { adminProductKeys } from "medusa-react"
import { LoaderFunctionArgs } from "react-router-dom"
import { AdminSalesChannelResponse } from "@medusajs/types"
import { client } from "../../../lib/client"
import { queryClient } from "../../../lib/medusa"
import { SalesChannelRes } from "../../../types/api-responses"
const salesChannelDetailQuery = (id: string) => ({
queryKey: adminProductKeys.detail(id),
@@ -15,7 +15,7 @@ export const salesChannelLoader = async ({ params }: LoaderFunctionArgs) => {
const query = salesChannelDetailQuery(id!)
return (
queryClient.getQueryData<SalesChannelRes>(query.queryKey) ??
queryClient.getQueryData<AdminSalesChannelResponse>(query.queryKey) ??
(await queryClient.fetchQuery(query))
)
}

View File

@@ -0,0 +1,32 @@
import { ApiKeyType } from "../../../api-key"
import { PaginatedResponse } from "../../../common"
/**
* @experimental
*/
interface ApiKeyResponse {
id: string
token: string
redacted: string
title: string
type: ApiKeyType
last_used_at: Date | null
created_by: string
created_at: Date
revoked_by: string | null
revoked_at: Date | null
}
/**
* @experimental
*/
export interface AdminApiKeyResponse {
api_key: ApiKeyResponse
}
/**
* @experimental
*/
export interface AdminApiKeyListResponse extends PaginatedResponse {
api_keys: ApiKeyResponse[]
}

View File

@@ -0,0 +1 @@
export * from "./api-key"

View File

@@ -0,0 +1 @@
export * from "./admin"

View File

@@ -0,0 +1 @@
export * from "./paginated-response"

View File

@@ -0,0 +1,5 @@
export interface PaginatedResponse {
limit: number
offset: number
count: number
}

View File

@@ -1,2 +1,4 @@
export * from "./stock-locations"
export * from "./api-key"
export * from "./fulfillment"
export * from "./sales-channel"
export * from "./stock-locations"

View File

@@ -0,0 +1 @@
export * from "./sales-channel"

View File

@@ -0,0 +1,26 @@
import { PaginatedResponse } from "../../common"
/**
* @experimental
*/
interface SalesChannelResponse {
id: string
name: string
description: string | null
is_disabled: boolean
metadata: Record<string, unknown> | null
}
/**
* @experimental
*/
export interface AdminSalesChannelListResponse extends PaginatedResponse {
sales_channels: SalesChannelResponse[]
}
/**
* @experimental
*/
export interface AdminSalesChannelResponse {
sales_channel: SalesChannelResponse
}

View File

@@ -0,0 +1 @@
export * from "./admin"