diff --git a/README.md b/README.md index 9fdc27cd..5081990d 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ See [docs/chromatic-setup.md](docs/chromatic-setup.md) for more details on our C #### Update PR Branches We provide a GitHub Actions workflow to automatically update open PR branches with the latest changes from `main`. This is useful for: + - Keeping long-running PRs up-to-date - Reducing merge conflicts - Repository maintenance diff --git a/pages/admin/add-unclaimed-node.tsx b/app/admin/add-unclaimed-node/page.tsx similarity index 85% rename from pages/admin/add-unclaimed-node.tsx rename to app/admin/add-unclaimed-node/page.tsx index edf38788..a9046960 100644 --- a/pages/admin/add-unclaimed-node.tsx +++ b/app/admin/add-unclaimed-node/page.tsx @@ -1,12 +1,11 @@ +'use client' import { Breadcrumb } from 'flowbite-react' -import { useRouter } from 'next/router' +import { useRouter } from 'next/navigation' import { HiHome } from 'react-icons/hi' import withAdmin from '@/components/common/HOC/authAdmin' import { AdminCreateNodeFormModal } from '@/components/nodes/AdminCreateNodeFormModal' import { useNextTranslation } from '@/src/hooks/i18n' -export default withAdmin(AddUnclaimedNodePage) - function AddUnclaimedNodePage() { const { t } = useNextTranslation() const router = useRouter() @@ -53,3 +52,9 @@ function AddUnclaimedNodePage() { ) } + +// TODO: Re-enable withAdmin after migrating HOC to App Router +// const Wrapped = withAdmin(AddUnclaimedNodePage) +export default AddUnclaimedNodePage + +export const dynamic = 'force-dynamic' diff --git a/pages/admin/claim-nodes.tsx b/app/admin/claim-nodes/page.tsx similarity index 87% rename from pages/admin/claim-nodes.tsx rename to app/admin/claim-nodes/page.tsx index b7463ebc..b3cc1d76 100644 --- a/pages/admin/claim-nodes.tsx +++ b/app/admin/claim-nodes/page.tsx @@ -1,6 +1,7 @@ +'use client' import { useQueryClient } from '@tanstack/react-query' import { Breadcrumb, Button, Spinner } from 'flowbite-react' -import { useRouter } from 'next/router' +import { useRouter, useSearchParams } from 'next/navigation' import { HiHome, HiPlus } from 'react-icons/hi' import { CustomPagination } from '@/components/common/CustomPagination' import withAdmin from '@/components/common/HOC/authAdmin' @@ -12,27 +13,26 @@ import { import { UNCLAIMED_ADMIN_PUBLISHER_ID } from '@/src/constants' import { useNextTranslation } from '@/src/hooks/i18n' -export default withAdmin(ClaimNodesPage) function ClaimNodesPage() { const { t } = useNextTranslation() const router = useRouter() + const searchParams = useSearchParams() const queryClient = useQueryClient() const pageSize = 36 + // Get page from URL query params, defaulting to 1 - const currentPage = router.query.page - ? parseInt(router.query.page as string, 10) + const currentPage = searchParams?.get('page') + ? parseInt(searchParams?.get('page')!, 10) : 1 const handlePageChange = (page: number) => { // Update URL with new page parameter - router.push( - { pathname: router.pathname, query: { ...router.query, page } }, - undefined, - { shallow: true } - ) + const params = new URLSearchParams(searchParams?.toString()) + params.set('page', page.toString()) + router.push(`/admin/claim-nodes?${params.toString()}`) } - // Use the page from router.query for the API call + // Use the page from searchParams for the API call const { data, isError, isLoading } = useListNodesForPublisherV2( UNCLAIMED_ADMIN_PUBLISHER_ID, { page: currentPage, limit: pageSize } @@ -147,3 +147,10 @@ function ClaimNodesPage() { ) } + +// TODO: Re-enable withAdmin after migrating HOC to App Router +// const Wrapped = withAdmin(ClaimNodesPage) + +export default ClaimNodesPage + +export const dynamic = 'force-dynamic' diff --git a/pages/admin/node-version-compatibility.tsx b/app/admin/node-version-compatibility/page.tsx similarity index 98% rename from pages/admin/node-version-compatibility.tsx rename to app/admin/node-version-compatibility/page.tsx index 4c84ed3a..8b6c14be 100644 --- a/pages/admin/node-version-compatibility.tsx +++ b/app/admin/node-version-compatibility/page.tsx @@ -1,3 +1,4 @@ +'use client' import { useQueryClient } from '@tanstack/react-query' import clsx from 'clsx' import { @@ -12,7 +13,7 @@ import { TextInput, Tooltip, } from 'flowbite-react' -import router from 'next/router' +import { useRouter } from 'next/navigation' import DIE, { DIES } from 'phpdie' import React, { Suspense, useEffect, useMemo, useState } from 'react' import { HiHome } from 'react-icons/hi' @@ -50,10 +51,9 @@ import { useSearchParameter } from '@/src/hooks/useSearchParameter' import { NodeVersionStatusToReadable } from '@/src/mapper/nodeversion' // This page allows admins to update node version compatibility fields -export default withAdmin(NodeVersionCompatibilityAdmin) - function NodeVersionCompatibilityAdmin() { const { t } = useNextTranslation() + const router = useRouter() const [_page, setPage] = usePage() // search @@ -636,3 +636,10 @@ function isNodeCompatibilityInfoOutdated(node: Node | null) { false ) } + +// TODO: Re-enable withAdmin after migrating HOC to App Router +// const Wrapped = withAdmin(NodeVersionCompatibilityAdmin) + +export default NodeVersionCompatibilityAdmin + +export const dynamic = 'force-dynamic' diff --git a/pages/admin/nodes.tsx b/app/admin/nodes/page.tsx similarity index 92% rename from pages/admin/nodes.tsx rename to app/admin/nodes/page.tsx index 8dfcf1bf..0697aa3f 100644 --- a/pages/admin/nodes.tsx +++ b/app/admin/nodes/page.tsx @@ -1,3 +1,4 @@ +'use client' import { useQueryClient } from '@tanstack/react-query' import clsx from 'clsx' import { @@ -10,9 +11,9 @@ import { TextInput, } from 'flowbite-react' import Link from 'next/link' -import { useRouter } from 'next/router' +import { useRouter, useSearchParams } from 'next/navigation' import { omit } from 'rambda' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { HiHome, HiOutlineX, HiPencil } from 'react-icons/hi' import { MdOpenInNew } from 'react-icons/md' import { toast } from 'react-toastify' @@ -32,6 +33,7 @@ import { useNextTranslation } from '@/src/hooks/i18n' function NodeList() { const { t } = useNextTranslation() const router = useRouter() + const searchParams = useSearchParams() const [page, setPage] = React.useState(1) const [editingNode, setEditingNode] = useState(null) const [editFormData, setEditFormData] = useState({ @@ -44,10 +46,11 @@ function NodeList() { // Handle page from URL React.useEffect(() => { - if (router.query.page) { - setPage(parseInt(router.query.page as string)) + const pageParam = searchParams?.get('page') + if (pageParam) { + setPage(parseInt(pageParam)) } - }, [router.query.page]) + }, [searchParams]) // Status filter functionality const statusFlags = { @@ -73,7 +76,7 @@ function NodeList() { const allStatuses = [...Object.values(statusFlags)].sort() const defaultSelectedStatuses = [ - (router.query as any)?.status ?? Object.keys(statusFlags), + searchParams?.get('status') ?? Object.keys(statusFlags), ] .flat() .map((status) => statusFlags[status]) @@ -91,29 +94,28 @@ function NodeList() { const checkedAll = allStatuses.join(',').toString() === [...statuses].sort().join(',').toString() - const searchParams = checkedAll - ? undefined - : ({ - status: Object.entries(statusFlags) - .filter(([status, s]) => statuses.includes(s)) - .map(([status]) => status), - } as any) - const search = new URLSearchParams({ - ...(omit('status')(router.query) as object), - ...searchParams, - }) - .toString() - .replace(/^(?!$)/, '?') - const hash = router.asPath.split('#')[1] - ? `#${router.asPath.split('#')[1]}` - : '' - router.push(`${router.pathname}${search}${hash}`) + + const params = new URLSearchParams(searchParams?.toString()) + + if (!checkedAll) { + // Remove existing status params + params.delete('status') + // Add new status params + Object.entries(statusFlags) + .filter(([status, s]) => statuses.includes(s)) + .forEach(([status]) => { + params.append('status', status) + }) + } else { + params.delete('status') + } + + const hash = window.location.hash + router.push(`/admin/nodes?${params.toString()}${hash}`) } // Search filter - const queryForNodeId = Array.isArray(router.query.nodeId) - ? router.query.nodeId[0] - : router.query.nodeId + const queryForNodeId = searchParams?.get('nodeId') const getAllNodesQuery = useListAllNodes({ page: page, @@ -163,14 +165,9 @@ function NodeList() { const handlePageChange = (newPage: number) => { setPage(newPage) - router.push( - { - pathname: router.pathname, - query: { ...router.query, page: newPage }, - }, - undefined, - { shallow: true } - ) + const params = new URLSearchParams(searchParams?.toString()) + params.set('page', newPage.toString()) + router.push(`/admin/nodes?${params.toString()}`) } const openEditModal = (node: Node) => { @@ -305,16 +302,16 @@ function NodeList() { 'filter-node-id' ) as HTMLInputElement const nodeId = inputElement.value.trim() - const searchParams = new URLSearchParams({ - ...(omit(['nodeId'])(router.query) as object), - ...(nodeId ? { nodeId } : {}), - }) - .toString() - .replace(/^(?!$)/, '?') - const hash = router.asPath.split('#')[1] - ? `#${router.asPath.split('#')[1]}` - : '' - router.push(router.pathname + searchParams + hash) + const params = new URLSearchParams(searchParams?.toString()) + + if (nodeId) { + params.set('nodeId', nodeId) + } else { + params.delete('nodeId') + } + + const hash = window.location.hash + router.push(`/admin/nodes?${params.toString()}${hash}`) }} > (1) const [selectedVersions, setSelectedVersions] = useState<{ [key: string]: boolean @@ -58,12 +60,13 @@ function NodeVersionList({}) { // Contact button, send issues or email to node version publisher const [mailtoNv, setMailtoNv] = useState(null) - // todo: optimize this, use fallback value instead of useEffect + // Handle page from URL React.useEffect(() => { - if (router.query.page) { - setPage(parseInt(router.query.page as string)) + const pageParam = searchParams?.get('page') + if (pageParam) { + setPage(parseInt(pageParam)) } - }, [router.query.page]) + }, [searchParams]) // allows filter by search param like /admin/nodeversions?filter=flagged&filter=pending const flags = { @@ -72,7 +75,8 @@ function NodeVersionList({}) { deleted: NodeVersionStatus.NodeVersionStatusDeleted, pending: NodeVersionStatus.NodeVersionStatusPending, active: NodeVersionStatus.NodeVersionStatusActive, - } satisfies Record // 'satisfies' requires latest typescript + } satisfies Record + const flagColors = { all: 'success', flagged: 'warning', @@ -92,7 +96,7 @@ function NodeVersionList({}) { const allFlags = [...Object.values(flags)].sort() const defaultSelectedStatus = [ - (router.query as any)?.filter ?? Object.keys(flags), + searchParams?.get('filter') ?? Object.keys(flags), ] .flat() .map((flag) => flags[flag]) @@ -106,32 +110,30 @@ function NodeVersionList({}) { const checkedAll = allFlags.join(',').toString() === [...status].sort().join(',').toString() - const searchParams = checkedAll - ? undefined - : ({ - filter: Object.entries(flags) - .filter(([flag, s]) => status.includes(s)) - .map(([flag]) => flag), - } as any) - const search = new URLSearchParams({ - ...(omit('filter')(router.query) as object), - ...searchParams, - }) - .toString() - .replace(/^(?!$)/, '?') - const hash = router.asPath.split('#')[1] - ? `#${router.asPath.split('#')[1]}` - : '' - router.push(`${router.pathname}${search}${hash}`) + + const params = new URLSearchParams(searchParams?.toString()) + + if (!checkedAll) { + params.delete('filter') + Object.entries(flags) + .filter(([flag, s]) => status.includes(s)) + .forEach(([flag]) => { + params.append('filter', flag) + }) + } else { + params.delete('filter') + } + + const hash = window.location.hash + router.push(`/admin/nodeversions?${params.toString()}${hash}`) } const [isAdminCreateNodeModalOpen, setIsAdminCreateNodeModalOpen] = useState(false) - const queryForNodeId = Array.isArray(router.query.nodeId) - ? router.query.nodeId[0] - : router.query.nodeId - const queryForStatusReason = router.query.statusReason as string + const queryForNodeId = searchParams?.get('nodeId') + const queryForStatusReason = searchParams?.get('statusReason') + const queryForVersion = searchParams?.get('version') const getAllNodeVersionsQuery = useListAllNodeVersions({ page: page, @@ -142,9 +144,6 @@ function NodeVersionList({}) { nodeId: queryForNodeId || undefined, }) - // todo: also implement this in the backend - const queryForVersion = router.query.version as string - const versions = (getAllNodeVersionsQuery.data?.versions || [])?.filter((nv) => { if (queryForVersion) return nv.version === queryForVersion @@ -169,15 +168,15 @@ function NodeVersionList({}) { nodeVersion: NodeVersion status: NodeVersionStatus message: string - batchId?: string // Optional batchId for batch operations + batchId?: string }) { // parse previous status reason with fallbacks const prevStatusReasonJson = parseJsonSafe(nv.status_reason).data const prevStatusReason = zStatusReason.safeParse(prevStatusReasonJson).data const previousHistory = prevStatusReason?.statusHistory ?? [] - const previousStatus = nv.status ?? 'Unknown Status' // should not happen - const previousMessage = prevStatusReason?.message ?? nv.status_reason ?? '' // use raw msg if fail to parse json - const previousBy = prevStatusReason?.by ?? 'admin@comfy.org' // unknown admin + const previousStatus = nv.status ?? 'Unknown Status' + const previousMessage = prevStatusReason?.message ?? nv.status_reason ?? '' + const previousBy = prevStatusReason?.by ?? 'admin@comfy.org' // concat history const statusHistory = [ @@ -188,14 +187,13 @@ function NodeVersionList({}) { by: previousBy, }, ] - // console.log('History', statusHistory) // updated status reason, with history and optionally batchId const reason = zStatusReason.parse({ message, - by: user?.email ?? 'admin@comfy.org', // if user is not loaded, use 'Admin' + by: user?.email ?? 'admin@comfy.org', statusHistory, - ...(batchId ? { batchId } : {}), // Include batchId if provided + ...(batchId ? { batchId } : {}), }) await updateNodeVersionMutation.mutateAsync( { @@ -244,9 +242,9 @@ function NodeVersionList({}) { const prevStatusReasonJson = parseJsonSafe(nv.status_reason).data const prevStatusReason = zStatusReason.safeParse(prevStatusReasonJson).data const previousHistory = prevStatusReason?.statusHistory ?? [] - const previousStatus = nv.status ?? 'Unknown Status' // should not happen - const previousMessage = prevStatusReason?.message ?? nv.status_reason ?? '' // use raw msg if fail to parse json - const previousBy = prevStatusReason?.by ?? 'admin@comfy.org' // unknown admin + const previousStatus = nv.status ?? 'Unknown Status' + const previousMessage = prevStatusReason?.message ?? nv.status_reason ?? '' + const previousBy = prevStatusReason?.by ?? 'admin@comfy.org' // concat history const statusHistory = [ @@ -263,7 +261,7 @@ function NodeVersionList({}) { message, by: user?.email ?? 'admin@comfy.org', statusHistory, - batchId, // Include the batchId for future undo-a-batch functionality + batchId, }) await updateNodeVersionMutation.mutateAsync( @@ -302,9 +300,9 @@ function NodeVersionList({}) { const prevStatusReasonJson = parseJsonSafe(nv.status_reason).data const prevStatusReason = zStatusReason.safeParse(prevStatusReasonJson).data const previousHistory = prevStatusReason?.statusHistory ?? [] - const previousStatus = nv.status ?? 'Unknown Status' // should not happen - const previousMessage = prevStatusReason?.message ?? nv.status_reason ?? '' // use raw msg if fail to parse json - const previousBy = prevStatusReason?.by ?? 'admin@comfy.org' // unknown admin + const previousStatus = nv.status ?? 'Unknown Status' + const previousMessage = prevStatusReason?.message ?? nv.status_reason ?? '' + const previousBy = prevStatusReason?.by ?? 'admin@comfy.org' // concat history const statusHistory = [ @@ -321,7 +319,7 @@ function NodeVersionList({}) { message, by: user?.email ?? 'admin@comfy.org', statusHistory, - batchId, // Include the batchId for future undo-a-batch functionality + batchId, }) await updateNodeVersionMutation.mutateAsync( @@ -378,7 +376,7 @@ function NodeVersionList({}) { nodeVersion: nv, status: NodeVersionStatus.NodeVersionStatusActive, message, - batchId, // Pass batchId to onReview if provided + batchId, }) toast.success( t('{{id}}@{{version}} Approved', { @@ -387,6 +385,7 @@ function NodeVersionList({}) { }) ) } + const onReject = async ( nv: NodeVersion, message?: string | null, @@ -408,7 +407,7 @@ function NodeVersionList({}) { nodeVersion: nv, status: NodeVersionStatus.NodeVersionStatusBanned, message, - batchId, // Pass batchId to onReview if provided + batchId, }) toast.success( t('{{id}}@{{version}} Rejected', { @@ -417,6 +416,7 @@ function NodeVersionList({}) { }) ) } + const checkIsUndoable = (nv: NodeVersion) => !!zStatusReason.safeParse(parseJsonSafe(nv.status_reason).data).data ?.statusHistory?.length @@ -442,51 +442,6 @@ function NodeVersionList({}) { ) return } - - // todo: search for this batchId and get a list of nodeVersions - // - // and show the list for confirmation - // - // and undo all of them - - // // Ask for confirmation - // if ( - // !confirm( - // `Do you want to undo the entire batch with ID: ${statusReason.batchId}?` - // ) - // ) { - // return - // } - - // const batchId = statusReason.batchId - - // // Find all node versions in the current view that have the same batch ID - // const batchNodes = versions.filter((v) => { - // const vStatusReason = zStatusReason.safeParse( - // parseJsonSafe(v.status_reason).data - // ).data - // return vStatusReason?.batchId === batchId - // }) - - // if (batchNodes.length === 0) { - // toast.error(`No nodes found with batch ID: ${batchId}`) - // return - // } - - // toast.info( - // `Undoing batch with ID: ${batchId} (${batchNodes.length} nodes)` - // ) - - // // Process all items in the batch using the undo function - // await pMap( - // batchNodes, - // async (nodeVersion) => { - // await onUndo(nodeVersion) - // }, - // { concurrency: 5, stopOnError: false } - // ) - - // toast.success(`Successfully undid batch with ID: ${batchId}`) } const onUndo = async (nv: NodeVersion) => { @@ -502,7 +457,7 @@ function NodeVersionList({}) { ) const prevStatus = statusHistory[statusHistory.length - 1].status - const by = user?.email // the user who clicked undo + const by = user?.email if (!by) { toast.error(t('Unable to get user email, please reload and try again')) return @@ -569,7 +524,6 @@ function NodeVersionList({}) { return } - // setBatchAction('') setIsBatchModalOpen(true) } @@ -648,14 +602,9 @@ function NodeVersionList({}) { const handlePageChange = (newPage: number) => { setPage(newPage) - router.push( - { - pathname: router.pathname, - query: { ...router.query, page: newPage }, - }, - undefined, - { shallow: true } - ) + const params = new URLSearchParams(searchParams?.toString()) + params.set('page', newPage.toString()) + router.push(`/admin/nodeversions?${params.toString()}`) } const BatchOperationBar = () => { @@ -850,17 +799,22 @@ function NodeVersionList({}) { 'filter-node-version' ) as HTMLInputElement const [nodeId, version] = inputElement.value.split('@') - const searchParams = new URLSearchParams({ - ...(omit(['nodeId', 'version'])(router.query) as object), - ...(nodeId ? { nodeId } : {}), - ...(version ? { version } : {}), - }) - .toString() - .replace(/^(?!$)/, '?') - const hash = router.asPath.split('#')[1] - ? `#${router.asPath.split('#')[1]}` - : '' - router.push(router.pathname + searchParams + hash) + const params = new URLSearchParams(searchParams?.toString()) + + if (nodeId) { + params.set('nodeId', nodeId) + } else { + params.delete('nodeId') + } + + if (version) { + params.set('version', version) + } else { + params.delete('version') + } + + const hash = window.location.hash + router.push(`/admin/nodeversions?${params.toString()}${hash}`) }} > = Object.keys(flags).length ), @@ -931,7 +884,6 @@ function NodeVersionList({}) { key={flag} color={flagColors[flag]} className={clsx({ - // use tailwind add a filter set bright 50% if not selected 'brightness-50': !selectedStatus.includes(status), 'hover:brightness-100': !selectedStatus.includes(status), 'transition-all duration-200': true, @@ -1026,9 +978,6 @@ function NodeVersionList({}) {