diff --git a/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx b/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx
index 8a836b701cc..622161c0f39 100644
--- a/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx
+++ b/apps/studio/components/interfaces/Database/Tables/ColumnList.tsx
@@ -1,12 +1,6 @@
-import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { noop } from 'lodash'
-import { Check, ChevronLeft, Edit, MoreVertical, Plus, Search, Trash, X } from 'lucide-react'
-import Link from 'next/link'
-import { useState } from 'react'
-
import { PostgresColumn } from '@supabase/postgres-meta'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
-import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { NoSearchResults } from 'components/ui/NoSearchResults'
@@ -15,18 +9,30 @@ import { isTableLike } from 'data/table-editor/table-editor-types'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
+import { noop } from 'lodash'
+import { Check, Edit, MoreVertical, Plus, Search, Trash, X } from 'lucide-react'
+import { useState } from 'react'
import {
Button,
+ Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
- Input,
+ Table,
+ TableBody,
+ TableCell,
+ TableFooter,
+ TableHead,
+ TableHeader,
+ TableRow,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
+import { Input } from 'ui-patterns/DataInputs/Input'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
interface ColumnListProps {
@@ -40,7 +46,7 @@ export const ColumnList = ({
onEditColumn = noop,
onDeleteColumn = noop,
}: ColumnListProps) => {
- const { id: _id, ref } = useParams()
+ const { id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const { data: project } = useSelectedProjectQuery()
@@ -62,7 +68,9 @@ export const ColumnList = ({
const columns =
(filterString.length === 0
? (selectedTable?.columns ?? [])
- : selectedTable?.columns?.filter((column) => column.name.includes(filterString))) ?? []
+ : selectedTable?.columns?.filter((column) =>
+ column.name.toLowerCase().includes(filterString.toLowerCase())
+ )) ?? []
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
const { can: canUpdateColumns } = useAsyncCheckPermissions(
@@ -72,16 +80,13 @@ export const ColumnList = ({
return (
-
-
-
} style={{ padding: '5px' }}>
-
-
+
+
setFilterString(e.target.value)}
+ onChange={(e) => setFilterString(e.target.value)}
icon={}
/>
@@ -108,115 +113,163 @@ export const ColumnList = ({
)}
- {isLoading &&
}
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+
+
+ Name
+
+
+ Data Type
+
+
+ Format
+
+
+ Nullable
+
+
+
+
+
+ {isError && (
+
+
+
+
+
+ )}
- {isError && (
-
- )}
+ {isSuccess && columns.length === 0 && filterString.length > 0 && (
+
+
+ setFilterString('')}
+ />
+
+
+ )}
- {isSuccess && (
- <>
- {columns.length === 0 ? (
- setFilterString('')}
- />
- ) : (
-
-
Name,
-
- Description
- ,
- Data Type,
- Format,
-
- Nullable
- ,
- ,
- ]}
- body={columns.map((x) => (
-
-
- {x.name}
-
-
- {x.comment !== null ? (
- {x.comment}
+ {isSuccess && columns.length === 0 && filterString.length === 0 && (
+
+
+ No columns created yet
+
+ There are no columns in "{selectedTable?.schema}.{selectedTable?.name}"
+
+
+
+ )}
+
+ {isSuccess &&
+ columns.length > 0 &&
+ columns.map((column) => (
+
+
+
+
{column.name}
+ {column.comment !== null ? (
+
+ {column.comment}
+
+ ) : null}
+
+
+
+ {column.data_type}
+
+
+ {column.format}
+
+
+ {column.is_nullable ? (
+
+
+
) : (
- No description
+
+
+
)}
-
-
- {x.data_type}
-
-
- {x.format}
-
-
- {x.is_nullable ? (
-
- ) : (
-
- )}
-
-
+
+
{!isSchemaLocked && isTableEntity && (
-
-
- } />
-
-
-
-
- onEditColumn(x)}
- className="space-x-2"
- >
-
- Edit column
-
-
- {!canUpdateColumns && (
-
- Additional permissions required to edit column
-
- )}
-
-
-
-
- onDeleteColumn(x)}
- className="space-x-2"
- >
-
- Delete column
-
-
- {!canUpdateColumns && (
-
- Additional permissions required to delete column
-
- )}
-
-
-
+
+
onEditColumn(column)}
+ tooltip={{
+ content: {
+ side: 'bottom',
+ text: !canUpdateColumns
+ ? 'Additional permissions required to edit column'
+ : undefined,
+ },
+ }}
+ >
+ Edit
+
+
+
+ } />
+
+
+
+
+ onDeleteColumn(column)}
+ className="space-x-2"
+ >
+
+ Delete column
+
+
+ {!canUpdateColumns && (
+
+ Additional permissions required to delete column
+
+ )}
+
+
+
+
)}
-
-
+
+
))}
- />
-
- )}
- >
- )}
+
+ {isSuccess && (
+
+
+
+ {columns.length} {columns.length === 1 ? 'column' : 'columns'}
+
+
+
+ )}
+
+ )}
+
)
}
diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx
index 298047e4084..a8bf7d7106e 100644
--- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx
+++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx
@@ -1,24 +1,5 @@
import type { PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { noop } from 'lodash'
-import {
- Check,
- Columns,
- Copy,
- Edit,
- Eye,
- Filter,
- MoreVertical,
- Plus,
- Search,
- Trash,
- X,
-} from 'lucide-react'
-import Link from 'next/link'
-import { useRouter } from 'next/router'
-import { parseAsString, useQueryState } from 'nuqs'
-import { useState } from 'react'
-
import { useParams } from 'common'
import { buildTableEditorUrl } from 'components/grid/SupabaseGrid.utils'
import AlertError from 'components/ui/AlertError'
@@ -37,6 +18,12 @@ import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
+import { noop } from 'lodash'
+import { Check, Copy, Edit, Eye, Filter, MoreVertical, Plus, Search, Trash, X } from 'lucide-react'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { parseAsString, useQueryState } from 'nuqs'
+import { useState } from 'react'
import {
Button,
Card,
@@ -47,12 +34,13 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
Label_Shadcn_,
+ Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
- Popover_Shadcn_,
Table,
TableBody,
TableCell,
+ TableFooter,
TableHead,
TableHeader,
TableRow,
@@ -62,6 +50,7 @@ import {
} from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
import { formatAllEntities } from './Tables.utils'
@@ -318,17 +307,19 @@ export const TableList = ({
- Name
-
- Description
+
+ Name
-
+
+ Columns
+
+
Rows (Estimated)
-
+
Size (Estimated)
-
+
Realtime Enabled
@@ -387,10 +378,6 @@ export const TableList = ({
- {/* [Alaister]: EntityTypeIcon supports PARTITIONED_TABLE, but formatAllEntities
- doesn't distinguish between tables and partitioned tables yet.
- Once the endpoint/formatAllEntities is updated to include partitioned tables,
- EntityTypeIcon will automatically style them correctly. */}
@@ -398,66 +385,70 @@ export const TableList = ({
-
- {/* only show tooltips if required, to reduce noise */}
- {x.name.length > 20 ? (
-
-
- {x.name}
-
+
+
+ {/* only show tooltips if required, to reduce noise */}
+ {x.name.length > 20 ? (
+
+
+ {x.name}
+
- {x.name}
-
+
{x.name}
+
+ ) : (
+
{x.name}
+ )}
+ {x.comment !== null ? (
+
+ {x.comment}
+
+ ) : null}
+
+
+
+
+ {x.columns.length.toLocaleString()}
+
+
+
+ {x.rows !== undefined ? (
+ {x.rows.toLocaleString()}
) : (
- {x.name}
+ –
)}
-
- {x.comment !== null ? (
-
- {x.comment}
-
- ) : (
- No description
- )}
-
-
- {x.rows !== undefined ? x.rows.toLocaleString() : '-'}
-
-
+
{x.size !== undefined ? (
- {x.size}
+ {x.size}
) : (
- '-'
+ –
)}
-
+
{(realtimePublication?.tables ?? []).find(
(table) => table.id === x.id
) ? (
-
+
) : (
-
+
)}
-
}
- className="whitespace-nowrap hover:border-muted"
- style={{ paddingTop: 3, paddingBottom: 3 }}
- >
+
@@ -554,6 +545,13 @@ export const TableList = ({
))}
>
+
+
+
+ {entities.length} {entities.length === 1 ? 'table' : 'tables'}
+
+
+
diff --git a/apps/studio/pages/project/[ref]/database/tables/[id].tsx b/apps/studio/pages/project/[ref]/database/tables/[id].tsx
index 94b4a029eda..e64808aa785 100644
--- a/apps/studio/pages/project/[ref]/database/tables/[id].tsx
+++ b/apps/studio/pages/project/[ref]/database/tables/[id].tsx
@@ -4,21 +4,21 @@ import DeleteConfirmationDialogs from 'components/interfaces/TableGridEditor/Del
import { SidePanelEditor } from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor'
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
-import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
-import { FormHeader } from 'components/ui/Forms/FormHeader'
+import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { isTableLike } from 'data/table-editor/table-editor-types'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
-import { ChevronRight } from 'lucide-react'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { TableEditorTableStateContextProvider } from 'state/table-editor-table'
import type { NextPageWithLayout } from 'types'
+import { PageContainer } from 'ui-patterns/PageContainer'
+import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
const DatabaseTables: NextPageWithLayout = () => {
const snap = useTableEditorStateSnapshot()
- const { id: _id } = useParams()
+ const { id: _id, ref } = useParams()
const id = _id ? Number(_id) : undefined
const { data: project } = useSelectedProjectQuery()
@@ -30,26 +30,28 @@ const DatabaseTables: NextPageWithLayout = () => {
return (
<>
-
-
-
-
-
-
- {isLoading ? (
-
- ) : (
-
- )}
-
-
-
-
-
+
: (selectedTable?.name ?? '')}
+ breadcrumbs={[
+ {
+ label: 'Tables',
+ href: `/project/${ref}/database/tables`,
+ },
+ ]}
+ size="large"
+ >
+
+
+
+
+
+
+
+
{project?.ref !== undefined && selectedTable !== undefined && isTableLike(selectedTable) && (
{
@@ -26,34 +21,29 @@ const DatabaseTables: NextPageWithLayout = () => {
return (
<>
-
-
-
- Database Tables
-
-
-
-
-
-
- {
- setSelectedTableToEdit(postgresTableToEntity(table))
- snap.onEditTable()
- }}
- onDeleteTable={(table) => {
- setSelectedTableToEdit(postgresTableToEntity(table))
- snap.onDeleteTable()
- }}
- onDuplicateTable={(table) => {
- setSelectedTableToEdit(postgresTableToEntity(table))
- snap.onDuplicateTable()
- }}
- />
-
-
-
+
+
+
+
+ {
+ setSelectedTableToEdit(postgresTableToEntity(table))
+ snap.onEditTable()
+ }}
+ onDeleteTable={(table) => {
+ setSelectedTableToEdit(postgresTableToEntity(table))
+ snap.onDeleteTable()
+ }}
+ onDuplicateTable={(table) => {
+ setSelectedTableToEdit(postgresTableToEntity(table))
+ snap.onDuplicateTable()
+ }}
+ />
+
+
+
+
{projectRef !== undefined &&
selectedTableToEdit !== undefined &&
diff --git a/e2e/studio/features/database.spec.ts b/e2e/studio/features/database.spec.ts
index ace3323d0d0..d8dfcf2085a 100644
--- a/e2e/studio/features/database.spec.ts
+++ b/e2e/studio/features/database.spec.ts
@@ -89,12 +89,14 @@ test.describe('Database', () => {
// validates table and column exists
await expect(page.getByText(databaseTableName, { exact: true })).toBeVisible()
// test we can edit the column
- await page.getByText(`${databaseTableName} actions`).click()
-
- await page.getByText(`${databaseTableName} actions`).click()
- await expect(page.getByRole('menuitem', { name: 'Edit table' })).toBeVisible()
- await page.getByRole('menuitem', { name: 'Edit table' }).click({ force: true })
- await expect(page.getByRole('menuitem', { name: 'Edit table' })).not.toBeVisible()
+ const tableActionsButton = page.getByRole('button', {
+ name: `${databaseTableName} actions`,
+ })
+ await tableActionsButton.click()
+ const editTableMenuItem = page.getByRole('menuitem', { name: 'Edit table' })
+ await expect(editTableMenuItem).toBeVisible()
+ await editTableMenuItem.press('Enter')
+ await expect(editTableMenuItem).not.toBeVisible()
const dialog = page.getByRole('dialog')
await expect(dialog).toBeVisible()
await expect(dialog.getByText('timestamptz')).toBeVisible()
@@ -104,10 +106,10 @@ test.describe('Database', () => {
await expect(page.getByRole('dialog')).not.toBeVisible()
// test the schema view has been refreshed
- await page.getByText(`${databaseTableName} actions`).click()
- await expect(page.getByRole('menuitem', { name: 'Edit table' })).toBeVisible()
- await page.getByRole('menuitem', { name: 'Edit table' }).click()
- await expect(page.getByRole('menuitem', { name: 'Edit table' })).not.toBeVisible()
+ await tableActionsButton.click()
+ await expect(editTableMenuItem).toBeVisible()
+ await editTableMenuItem.press('Enter')
+ await expect(editTableMenuItem).not.toBeVisible()
await expect(page.getByRole('dialog')).toBeVisible()
// FIXME: For some reason, the dialog is not stable and rerenders, sometimes preventing the description to be filled
await page.waitForTimeout(500)
@@ -115,15 +117,19 @@ test.describe('Database', () => {
await page.getByRole('button', { name: 'Cancel' }).click()
await expect(page.getByRole('dialog')).not.toBeVisible()
- await page.getByText(`${databaseTableName} actions`).click()
- await expect(page.getByRole('menuitem', { name: 'Copy name' })).toBeVisible()
- await page.getByRole('menuitem', { name: 'Copy name' }).click()
- await expect(page.getByRole('menuitem', { name: 'Copy name' })).not.toBeVisible()
+ await tableActionsButton.click()
+ const copyTableNameMenuItem = page.getByRole('menuitem', { name: 'Copy name' })
+ await expect(copyTableNameMenuItem).toBeVisible()
+ await copyTableNameMenuItem.press('Enter')
+ await expect(copyTableNameMenuItem).not.toBeVisible()
await expectClipboardValue({ page, value: databaseTableName, exact: true })
- await page.getByText(`${databaseTableName} actions`).click()
- await expect(page.getByRole('menuitem', { name: 'View in Table Editor' })).toBeVisible()
- await page.getByRole('menuitem', { name: 'View in Table Editor' }).click()
+ await tableActionsButton.click()
+ const viewInTableEditorMenuItem = page.getByRole('menuitem', {
+ name: 'View in Table Editor',
+ })
+ await expect(viewInTableEditorMenuItem).toBeVisible()
+ await viewInTableEditorMenuItem.press('Enter')
await page.waitForURL(/.*\/editor\/\d+/)
await expect(page.getByRole('tab', { name: databaseTableName })).toBeVisible()
})
@@ -152,11 +158,13 @@ test.describe('Database', () => {
await expect(page.getByText(databaseTableName, { exact: true })).toBeVisible()
await expect(page.getByText(databaseColumnName, { exact: true })).toBeVisible()
// test we can edit the column
- await page
- .getByText(`${databaseTableName} ${databaseColumnName} actions`)
- .click({ force: true })
- await expect(page.getByRole('menuitem', { name: 'Edit column' })).toBeVisible()
- await page.getByRole('menuitem', { name: 'Edit column' }).click()
+ const columnActionsButton = page.getByRole('button', {
+ name: `${databaseTableName} ${databaseColumnName} actions`,
+ })
+ await columnActionsButton.click()
+ const editColumnMenuItem = page.getByRole('menuitem', { name: 'Edit column' })
+ await expect(editColumnMenuItem).toBeVisible()
+ await editColumnMenuItem.press('Enter')
await page.getByLabel('Description').fill('Bazinga')
await page.getByRole('button', { name: 'Save' }).click()
await expect(
@@ -165,20 +173,17 @@ test.describe('Database', () => {
await expect(page.getByRole('dialog')).not.toBeVisible()
// test the schema view has been refreshed
- await page
- .getByText(`${databaseTableName} ${databaseColumnName} actions`)
- .click({ force: true })
- await expect(page.getByRole('menuitem', { name: 'Edit column' })).toBeVisible()
- await page.getByRole('menuitem', { name: 'Edit column' }).click()
+ await columnActionsButton.click()
+ await expect(editColumnMenuItem).toBeVisible()
+ await editColumnMenuItem.press('Enter')
await expect(page.getByLabel('Description')).toHaveValue('Bazinga')
await page.getByRole('button', { name: 'Cancel' }).click()
await expect(page.getByRole('dialog')).not.toBeVisible()
- await page
- .getByText(`${databaseTableName} ${databaseColumnName} actions`)
- .click({ force: true })
- await expect(page.getByRole('menuitem', { name: 'Copy name' })).toBeVisible()
- await page.getByRole('menuitem', { name: 'Copy name' }).click()
+ await columnActionsButton.click()
+ const copyColumnNameMenuItem = page.getByRole('menuitem', { name: 'Copy name' })
+ await expect(copyColumnNameMenuItem).toBeVisible()
+ await copyColumnNameMenuItem.press('Enter')
await expectClipboardValue({ page, value: databaseColumnName, exact: true })
})
})
@@ -208,11 +213,13 @@ test.describe('Database', () => {
await expect(page.getByRole('button', { name: 'New table' })).toBeVisible()
// validates database name is present and has accurate number of columns
- const tableRow = page.getByRole('row', {
- name: `${databaseTableName} No description`,
- })
+ const tableRow = page
+ .getByRole('row')
+ .filter({ has: page.getByText(databaseTableName, { exact: true }) })
+ .first()
await expect(tableRow).toContainText(databaseTableName)
- await expect(tableRow).toContainText('3 columns')
+ await expect(tableRow.getByRole('cell').filter({ hasText: /^3$/ }).first()).toBeVisible()
+ await expect(tableRow.getByRole('link', { name: 'View columns' })).toBeVisible()
// change schema -> auth
await page.getByTestId('schema-selector').click()
@@ -364,14 +371,20 @@ test.describe('Database', () => {
}
)
+ const databaseWait = createApiResponseWaiter(
+ page,
+ 'pg-meta',
+ ref,
+ 'tables?include_columns=true&included_schemas=public'
+ )
await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/tables?schema=public`))
// Wait for database tables to be populated
- await waitForDatabaseToLoad(page, ref)
+ await databaseWait
// navigate to table columns
const databaseRow = page.getByRole('row', { name: databaseTableName })
- await databaseRow.getByRole('link', { name: '3 columns' }).click()
+ await databaseRow.getByRole('link', { name: 'View columns' }).click()
await page.waitForURL(/.*\/database\/tables\/\d+/)
// validate and display everything correctly
@@ -407,8 +420,7 @@ test.describe('Database', () => {
await expect(columnDatabase2Row).toContainText('numeric')
// update table column
- await columnDatabase2Row.getByRole('button').click()
- await page.getByRole('button', { name: 'Edit column' }).click()
+ await columnDatabase2Row.getByRole('button', { name: 'Edit' }).click()
await page.getByLabel('name').fill(databaseColumnName3)
const columnUpdateWait = createApiResponseWaiter(
page,
@@ -430,8 +442,8 @@ test.describe('Database', () => {
// delete table column
const columnDatabase3Row = page.getByRole('row', { name: databaseColumnName3 })
- await columnDatabase3Row.getByRole('button').click()
- await page.getByRole('button', { name: 'Delete column' }).click()
+ await columnDatabase3Row.getByRole('button').last().click()
+ await page.getByRole('menuitem', { name: 'Delete column' }).click()
await page.getByRole('checkbox', { name: 'Drop column with cascade?' }).check()
const columnDeleteWait = createApiResponseWaiter(
page,
diff --git a/e2e/studio/utils/wait-for-response.ts b/e2e/studio/utils/wait-for-response.ts
index c40feb5566c..0a0ab48ad4e 100644
--- a/e2e/studio/utils/wait-for-response.ts
+++ b/e2e/studio/utils/wait-for-response.ts
@@ -24,19 +24,25 @@ function buildUrlMatcher(basePath: string, ref: string, action: string, method?:
// Normalize inputs and build a tolerant matcher that works across environments
const trimmedBasePath = basePath.replace(/^\/+|\/+$/g, '')
const refAlternatives = [ref, 'default']
+ const [actionPath, actionQuery] = action.split('?')
+ const trimmedActionPath = actionPath.replace(/^\/+/, '')
+ const expectedSearchParams = new URLSearchParams(actionQuery ?? '')
return (response: any) => {
- const url = response.url()
+ const url = new URL(response.url())
const requestMethod = response.request().method()
// Must include base path and one of the ref alternatives
- const hasBasePath = url.includes(`${trimmedBasePath}/`)
- const hasRef = refAlternatives.some((r) => url.includes(`/${r}/`))
+ const hasBasePath = url.pathname.includes(`/${trimmedBasePath}/`)
+ const hasRef = refAlternatives.some((r) => url.pathname.includes(`/${r}/`))
- // Action match should be tolerant to extra query params ordering
- const hasAction = url.includes(action)
+ const hasActionPath =
+ trimmedActionPath.length === 0 || url.pathname.includes(`/${trimmedActionPath}`)
+ const hasExpectedSearchParams = [...expectedSearchParams.entries()].every(([key, value]) =>
+ url.searchParams.getAll(key).some((actualValue) => actualValue.includes(value))
+ )
- const urlMatches = hasBasePath && hasRef && hasAction
+ const urlMatches = hasBasePath && hasRef && hasActionPath && hasExpectedSearchParams
if (method) return urlMatches && requestMethod === method
return urlMatches
}