refinedev/refine

[BUG] Resource routes using path param other than id not being substituted in URL

Closed this issue · 1 comments

Describe the bug

We would like to route based on a unique slug using react router and the refine resource definitions rather than the ids to have more readable URLs. When we try and use a resource path such as show: "/publishers/show/:handle" it doesn't substitute the handle field but rather puts :handle directly in the URL.

Do Refine resources only accept the :id path param?

Example App.tsx after bootstrap

import { Authenticated, GitHubBanner, Refine } from "@refinedev/core";
import { DevtoolsPanel, DevtoolsProvider } from "@refinedev/devtools";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";

import {
  AuthPage,
  ErrorComponent,
  ThemedLayoutV2,
  ThemedSiderV2,
  useNotificationProvider,
} from "@refinedev/antd";
import "@refinedev/antd/dist/reset.css";

import routerBindings, {
  CatchAllNavigate,
  DocumentTitleHandler,
  NavigateToResource,
  UnsavedChangesNotifier,
} from "@refinedev/react-router-v6";
import { dataProvider, liveProvider } from "@refinedev/supabase";
import { App as AntdApp } from "antd";
import { BrowserRouter, Outlet, Route, Routes } from "react-router-dom";
import authProvider from "./authProvider";
import { Header } from "./components/header";
import { ColorModeContextProvider } from "./contexts/color-mode";
import { supabaseClient } from "./utility";
import { PublisherCreate, PublishersEdit, PublisherList, PublisherShow } from "./pages/publishers";

function App() {
  return (
    (<BrowserRouter>
      <GitHubBanner />
      <RefineKbarProvider>
        <ColorModeContextProvider>
          <AntdApp>
            <DevtoolsProvider>
              <Refine
                dataProvider={dataProvider(supabaseClient)}
                liveProvider={liveProvider(supabaseClient)}
                authProvider={authProvider}
                routerProvider={routerBindings}
                notificationProvider={useNotificationProvider}
                resources={[{
                  name: "publisher",
                  list: "/publishers",
                  show: "/publishers/show/:handle",
                  meta: {
                    label: 'Publishers',
                  }
                }]}
                options={{
                  syncWithLocation: true,
                  warnWhenUnsavedChanges: true,
                  useNewQueryKeys: true,
                  projectId: "******",
                }}
              >
                <Routes>
                  <Route
                    element={
                      <Authenticated
                        key="authenticated-inner"
                        fallback={<CatchAllNavigate to="/login" />}
                      >
                        <ThemedLayoutV2
                          Header={Header}
                          Sider={(props) => <ThemedSiderV2 {...props} fixed />}
                        >
                          <Outlet />
                        </ThemedLayoutV2>
                      </Authenticated>
                    }
                  >
                    <Route
                      index
                      element={<NavigateToResource resource="publisher" />}
                    />
                    <Route path="/publishers">
                      <Route index element={<PublisherList />} />
                      <Route path="show/:handle" element={<PublisherShow />} />
                    </Route>
                    <Route path="*" element={<ErrorComponent />} />
                  </Route>
                  <Route
                    element={
                      <Authenticated
                        key="authenticated-outer"
                        fallback={<Outlet />}
                      >
                        <NavigateToResource />
                      </Authenticated>
                    }
                  >
                    <Route
                      path="/login"
                      element={
                        <AuthPage
                          type="login"
                          formProps={{
                            initialValues: {
                              email: "info@refine.dev",
                              password: "refine-supabase",
                            },
                          }}
                        />
                      }
                    />
                    <Route
                      path="/register"
                      element={<AuthPage type="register" />}
                    />
                    <Route
                      path="/forgot-password"
                      element={<AuthPage type="forgotPassword" />}
                    />
                  </Route>
                </Routes>

                <RefineKbar />
                <UnsavedChangesNotifier />
                <DocumentTitleHandler />
              </Refine>
              <DevtoolsPanel />
            </DevtoolsProvider>
          </AntdApp>
        </ColorModeContextProvider>
      </RefineKbarProvider>
    </BrowserRouter>)
  );
}

export default App;

Example pages/publishers/list.tsx

import { useTable, List, EditButton, ShowButton } from "@refinedev/antd";
import { Table, Space } from "antd";
import { Publisher } from "../../types/publisher";

export const PublisherList = () => {
    const { tableProps } = useTable<Publisher>({ });

    return (
        <List>
            <Table {...tableProps} rowKey="handle">
                <Table.Column
                  dataIndex="id"
                  title="Id"
                />
                <Table.Column
                  dataIndex="publisher_name"
                  title="Publisher Name"
                />
                <Table.Column
                  dataIndex="handle"
                  title="Handle"
                />
                <Table.Column
                    title="Actions"
                    dataIndex="actions"
                    render={(_, record: Publisher) => (
                        <Space>
                            <EditButton
                                hideText
                                size="small"
                                recordItemId={record.handle}
                            />
                            <ShowButton
                                hideText
                                size="small"
                                recordItemId={record.handle}
                            />
                        </Space>
                    )}
                />
            </Table>
        </List>
    );
};

Example pages/publishers/show.tsx

import { Show, TextField } from "@refinedev/antd";
import { useShow } from "@refinedev/core";
import { Typography } from "antd";
import { Publisher } from "../../types/publisher";

const { Title } = Typography;

export const PublisherShow = () => {
  const { queryResult } = useShow<Publisher>({ });
  const { data, isLoading } = queryResult;

  const record = data?.data;

  return (
    <Show isLoading={isLoading}>
      <Title level={5}>{"ID"}</Title>
      <TextField value={record?.id} />
      <Title level={5}>{"Publisher Name"}</Title>
      <TextField value={record?.publisher_name} />
      <Title level={5}>{"Handle"}</Title>
      <TextField value={record?.handle} />
    </Show>
  );
};

Steps To Reproduce

  1. Use Refine CLI to bootstrap project with Vite, React Router, Supabase, and AntDesign
  2. Copy example App.tsx and publisher pages from bug description
  3. Replace projectId with your projectId
  4. Configure supabase client (an unlinked local project will work)
  5. Add publisher table with columns: id, publisher_name, handle into supabase
  6. Add row with publisher_name: "Test Publisher" and handle: "test-publisher"
  7. Start refine app and open in browser
  8. Find Test Publisher in table and click the view icon
  9. Note the url includes the raw string :handle

Expected behavior

Refine correctly substitutes the :handle param with the handle field from the entity in the URL.

Packages

  • @refinedev/antd
  • @refinedev/core
  • @refinedev/react-router-v6

Additional Context

No response

Found it. It looks like this is because ShowButton actually functions under the assumption that you're using the id param and only substitutes that. Quick fix for anyone interested would be to swizzle out that component or just use the onClick param, that seems to override the behavior. So something like:

                            <ShowButton
                                hideText
                                size="small"
                                onClick={() => go({ to: `show/${record.handle}` })}
                            />