Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/custom-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "custom-test",
"version": "1.0.0",
"description": "",
"keywords": [],
"license": "ISC",
"author": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}
7 changes: 7 additions & 0 deletions examples/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
plugins: [react()],
server: { host: true, port: 5173 },
});
32 changes: 32 additions & 0 deletions packages/core/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Refine } from "@refinedev/core";
import { BrowserRouter } from "react-router-dom";
import { RefineKbarProvider } from "@refinedev/kbar";
import { RefineSnackbarProvider, notificationProvider } from "@refinedev/mui";
import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import dataProvider from "@refinedev/simple-rest";

const TestDashboard = () => <h2>✅ Custom Dashboard Works!</h2>;

export default function App() {
return (
<BrowserRouter>
<RefineKbarProvider>
<RefineSnackbarProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<Refine
dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
notificationProvider={notificationProvider}
resources={[
{
name: "test",
custom: [{ path: "dashboard", component: TestDashboard }],
},
]}
/>
</RefineSnackbarProvider>
</RefineKbarProvider>
</BrowserRouter>
);
}
94 changes: 15 additions & 79 deletions packages/core/src/contexts/resource/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { ComponentType, ReactNode } from "react";

import type { UseQueryResult } from "@tanstack/react-query";

import type { ILogData } from "../auditLog/types";

/**
Expand Down Expand Up @@ -41,53 +39,19 @@ export type ResourceAuditLogPermissions =

/** Resource `meta` */
export interface KnownResourceMeta {
/**
* This is used when setting the document title, in breadcrumbs and `<Sider />` components.
* Therefore it will only work if the related components have implemented the `label` property.
*/
label?: string;
/**
* Whether to hide the resource from the sidebar or not.
* This property is checked by the `<Sider />` components.
* Therefore it will only work if the `<Sider />` component has implemented the `hide` property.
*/
hide?: boolean;
/**
* Dedicated data provider name for the resource.
* If not set, the default data provider will be used.
* You can use this property to pick a data provider for a resource when you have multiple data providers.
*/
dataProviderName?: string;
/**
* To nest a resource under another resource, set the parent property to the name of the parent resource.
* This will work even if the parent resource is not explicitly defined.
*/
parent?: string;
/**
* To determine if the resource has ability to delete or not.
*/
canDelete?: boolean;
/**
* To permit the audit log for actions on the resource.
* @default All actions are permitted to be logged.
*/
audit?: ResourceAuditLogPermissions[];
/**
* To pass `icon` to the resource.
*/
icon?: ReactNode;
}

export interface DeprecatedOptions {
/**
* @deprecated Please use `audit` property instead.
*/
auditLog?: {
permissions?: ResourceAuditLogPermissions[];
};
/**
* @deprecated Define the route in the resource components instead
*/
route?: string;
}

Expand All @@ -97,56 +61,31 @@ export interface ResourceMeta extends KnownResourceMeta {

export interface ResourceProps extends IResourceComponents {
name: string;
/**
* This property can be used to identify a resource. In some cases, `name` of the resource might be repeated in different resources.
* To avoid conflicts, you pass the `identifier` property to be used as the key of the resource.
* @default `name` of the resource
*/
identifier?: string;
/**
* @deprecated This property is not used anymore.
*/
key?: string;
/**
* @deprecated Please use the `meta` property instead.
*/
options?: ResourceMeta & DeprecatedOptions;
/**
* To configure the resource, you can set `meta` properties. You can use `meta` to store any data related to the resource.
* There are some known `meta` properties that are used by the core and extension packages.
*/
meta?: ResourceMeta & DeprecatedOptions;
/**
* @deprecated Please use the `meta.canDelete` property instead.
*/
canDelete?: boolean;
/**
* @deprecated Please use the `meta.icon` property instead
*/
icon?: ReactNode;
parentName?: string;

/**
* @deprecated Please use the `meta.parent` property instead
* ✅ New: Allows defining additional custom routes for this resource.
* Example:
* custom: [
* { path: "dashboard", component: DashboardPage },
* { path: "stats", component: StatsPage }
* ]
*/
parentName?: string;
custom?: Array<{ path: string; component: ComponentType<any> }>;
}

export interface RouteableProperties {
/**
* @deprecated Please use action props instead.
*/
canCreate?: boolean;
/**
* @deprecated Please use action props instead.
*/
canEdit?: boolean;
/**
* @deprecated Please use action props instead.
*/
canShow?: boolean;
/**
* @deprecated Please use the `meta.canDelete` property instead.
*/
canDelete?: boolean;
canList?: boolean;
}

export interface IResourceComponentsProps<
Expand All @@ -171,6 +110,11 @@ export interface IResourceItem
* @deprecated Please use action components and `getDefaultActionPath` helper instead.
*/
route?: string;

/**
* ✅ Custom non-CRUD routes for this resource.
*/
custom?: Array<{ path: string; component: ComponentType<any> }>;
}

export interface IResourceContext {
Expand All @@ -181,14 +125,6 @@ export type ResourceBindings = ResourceProps[];

type MetaProps<TExtends = { [key: string]: any }> = ResourceMeta & TExtends;

export interface RouteableProperties {
canCreate?: boolean;
canEdit?: boolean;
canShow?: boolean;
canDelete?: boolean;
canList?: boolean;
}

export interface IResourceContext {
resources: IResourceItem[];
}
Expand Down
36 changes: 31 additions & 5 deletions packages/react-router-v6/src/create-resource-routes.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from "react";
import type { Action, ResourceProps } from "@refinedev/core";

import { Route } from "react-router-dom";

export const createResourcePathWithAction = (
Expand Down Expand Up @@ -56,6 +55,7 @@ export const createResourceRoutes = (resources: ResourceProps[]) => {
path: string;
}[] = [];

// ✅ Existing CRUD route generation
(["list", "show", "edit", "create"] as const).forEach((action) => {
const item = resource[action];
if (typeof item !== "undefined" && typeof item !== "string") {
Expand All @@ -73,11 +73,37 @@ export const createResourceRoutes = (resources: ResourceProps[]) => {
}
});

return actions.map(({ action, element: Component, path }) => {
const element = <Component />;
// ✅ New: Add support for custom routes
const customRoutes =
(resource as any).custom?.map(
(
custom: { path: string; component: React.ComponentType<any> },
idx: number,
) => {
const CustomComponent = custom.component;
return (
<Route
key={`custom-${resource.name}-${idx}`}
path={`/${resource.name}/${custom.path}`}
element={<CustomComponent />}
/>
);
},
) || [];

return <Route key={`${action}-${path}`} path={path} element={element} />;
});
// Return CRUD routes + custom routes
return [
...actions.map(({ action, element: Component, path }) => {
return (
<Route
key={`${action}-${path}`}
path={path}
element={<Component />}
/>
);
}),
...customRoutes,
];
});

return routes;
Expand Down
Loading