This commit is contained in:
Stefan Pejcic
2024-11-07 19:03:37 +01:00
parent c6df945ed5
commit 09f9f9502d
2472 changed files with 620417 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
import React, { type PropsWithChildren } from "react";
import type {
IAccessControlContext,
IAccessControlContextReturnType,
} from "./types";
/** @deprecated default value for access control context has no use and is an empty object. */
export const defaultAccessControlContext = {} as IAccessControlContext;
export const AccessControlContext =
React.createContext<IAccessControlContextReturnType>({
options: {
buttons: { enableAccessControl: true, hideIfUnauthorized: false },
},
});
export const AccessControlContextProvider: React.FC<
PropsWithChildren<IAccessControlContext>
> = ({ can, children, options }) => {
return (
<AccessControlContext.Provider
value={{
can,
options: options
? {
...options,
buttons: {
enableAccessControl: true,
hideIfUnauthorized: false,
...options.buttons,
},
}
: {
buttons: {
enableAccessControl: true,
hideIfUnauthorized: false,
},
queryOptions: undefined,
},
}}
>
{children}
</AccessControlContext.Provider>
);
};

View File

@@ -0,0 +1,93 @@
/**
* @author aliemir
*
* `AccessControlProvider` interface, used to define the access control bindings of refine.
*
* Currently, there's no change in the interface, but only in the `params.resource` property.
*
* This also had `{ children?: ITreeMenu[] }` type extension but we can remove it now.
*
* There's an error behind this extension, since we're using `Tanstack Query` to check the `can` function,
* params are stringified and Nodes can't be stringified properly, which throws an error.
*
* These kinds of errors should be handled by the user of the `can` function, not by the `can` function itself.
*
* In this case, its the `CanAccess` component, which wraps the `can` function and is used in the `Sider` components.
* `Sider` should sanitize the `params.resource` property and remove the `children` property (if exists).
*
* This may also apply to `resource.icon` property.
*
*/
import type { UseQueryOptions } from "@tanstack/react-query";
import type { BaseKey } from "../data/types";
import type { IResourceItem, ITreeMenu } from "../resource/types";
export type CanResponse = {
can: boolean;
reason?: string;
[key: string]: unknown;
};
export type CanParams = {
/**
* Resource name for API data interactions
*/
resource?: string;
/**
* Intended action on resource
*/
action: string;
/**
* Parameters associated with the resource
* @type {
* resource?: [IResourceItem](https://refine.dev/docs/api-reference/core/interfaceReferences/#canparams),
* id?: [BaseKey](https://refine.dev/docs/api-reference/core/interfaceReferences/#basekey), [key: string]: any
* }
*/
params?: {
resource?: IResourceItem & { children?: ITreeMenu[] };
id?: BaseKey;
[key: string]: any;
};
};
export type CanReturnType = {
can: boolean;
reason?: string;
};
export type CanFunction = ({
resource,
action,
params,
}: CanParams) => Promise<CanReturnType>;
type AccessControlOptions = {
buttons?: {
enableAccessControl?: boolean;
hideIfUnauthorized?: boolean;
};
queryOptions?: UseQueryOptions<CanReturnType>;
};
export interface IAccessControlContext {
can?: CanFunction;
options?: AccessControlOptions;
}
export type IAccessControlContextReturnType = {
can?: CanFunction;
options: {
buttons: {
enableAccessControl: boolean;
hideIfUnauthorized: boolean;
};
queryOptions?: UseQueryOptions<CanReturnType>;
};
};
export type AccessControlProvider = {
can: CanFunction;
options?: AccessControlOptions;
};

View File

@@ -0,0 +1,15 @@
import React, { type PropsWithChildren } from "react";
import type { IAuditLogContext } from "./types";
export const AuditLogContext = React.createContext<IAuditLogContext>({});
export const AuditLogContextProvider: React.FC<
PropsWithChildren<IAuditLogContext>
> = ({ create, get, update, children }) => {
return (
<AuditLogContext.Provider value={{ create, get, update }}>
{children}
</AuditLogContext.Provider>
);
};

View File

@@ -0,0 +1,45 @@
import type { BaseKey, MetaDataQuery } from "../data/types";
export type ILog<TData = any> = {
id: BaseKey;
createdAt: string;
author?: Record<number | string, any>;
name?: string;
data: TData;
previousData: TData;
resource: string;
action: string;
meta?: Record<number | string, any>;
};
export type ILogData<TData = any> = ILog<TData>[];
export type LogParams = {
resource: string;
action: string;
data?: any;
author?: {
name?: string;
[key: string]: any;
};
previousData?: any;
meta: Record<number | string, any>;
};
export type IAuditLogContext = {
create?: (params: LogParams) => Promise<any>;
get?: (params: {
resource: string;
action?: string;
meta?: Record<number | string, any>;
author?: Record<number | string, any>;
metaData?: MetaDataQuery;
}) => Promise<any>;
update?: (params: {
id: BaseKey;
name: string;
[key: string]: any;
}) => Promise<any>;
};
export type AuditLogProvider = Required<IAuditLogContext>;

View File

@@ -0,0 +1,200 @@
import React, { type PropsWithChildren } from "react";
import { useNavigation } from "@hooks";
import type { IAuthContext, ILegacyAuthContext } from "./types";
/**
* @deprecated `LegacyAuthContext` is deprecated with refine@4, use `AuthBindingsContext` instead, however, we still support `LegacyAuthContext` for backward compatibility.
*/
export const LegacyAuthContext = React.createContext<ILegacyAuthContext>({});
/**
* @deprecated `LegacyAuthContextProvider` is deprecated with refine@4, use `AuthBindingsContextProvider` instead, however, we still support `LegacyAuthContextProvider` for backward compatibility.
*/
export const LegacyAuthContextProvider: React.FC<
PropsWithChildren<ILegacyAuthContext>
> = ({ children, isProvided, ...authOperations }) => {
const { replace } = useNavigation();
const loginFunc = async (params: any) => {
try {
const result = await authOperations.login?.(params);
return result;
} catch (error) {
return Promise.reject(error);
}
};
const registerFunc = async (params: any) => {
try {
const result = await authOperations.register?.(params);
return result;
} catch (error) {
return Promise.reject(error);
}
};
const logoutFunc = async (params: any) => {
try {
const redirectPath = await authOperations.logout?.(params);
return redirectPath;
} catch (error) {
return Promise.reject(error);
}
};
const checkAuthFunc = async (params: any) => {
try {
await authOperations.checkAuth?.(params);
return Promise.resolve();
} catch (error) {
if ((error as { redirectPath?: string })?.redirectPath) {
replace((error as { redirectPath: string }).redirectPath);
}
return Promise.reject(error);
}
};
return (
<LegacyAuthContext.Provider
value={{
...authOperations,
login: loginFunc,
logout: logoutFunc,
checkAuth: checkAuthFunc,
register: registerFunc,
isProvided,
}}
>
{children}
</LegacyAuthContext.Provider>
);
};
export const AuthBindingsContext = React.createContext<Partial<IAuthContext>>(
{},
);
export const AuthBindingsContextProvider: React.FC<
PropsWithChildren<IAuthContext>
> = ({ children, isProvided, ...authBindings }) => {
const handleLogin = async (params: unknown) => {
try {
const result = await authBindings.login?.(params);
return result;
} catch (error) {
console.warn(
"Unhandled Error in login: refine always expects a resolved promise.",
error,
);
return Promise.reject(error);
}
};
const handleRegister = async (params: unknown) => {
try {
const result = await authBindings.register?.(params);
return result;
} catch (error) {
console.warn(
"Unhandled Error in register: refine always expects a resolved promise.",
error,
);
return Promise.reject(error);
}
};
const handleLogout = async (params: unknown) => {
try {
const result = await authBindings.logout?.(params);
return result;
} catch (error) {
console.warn(
"Unhandled Error in logout: refine always expects a resolved promise.",
error,
);
return Promise.reject(error);
}
};
const handleCheck = async (params: unknown) => {
try {
const result = await authBindings.check?.(params);
return Promise.resolve(result);
} catch (error) {
console.warn(
"Unhandled Error in check: refine always expects a resolved promise.",
error,
);
return Promise.reject(error);
}
};
const handleForgotPassword = async (params: unknown) => {
try {
const result = await authBindings.forgotPassword?.(params);
return Promise.resolve(result);
} catch (error) {
console.warn(
"Unhandled Error in forgotPassword: refine always expects a resolved promise.",
error,
);
return Promise.reject(error);
}
};
const handleUpdatePassword = async (params: unknown) => {
try {
const result = await authBindings.updatePassword?.(params);
return Promise.resolve(result);
} catch (error) {
console.warn(
"Unhandled Error in updatePassword: refine always expects a resolved promise.",
error,
);
return Promise.reject(error);
}
};
return (
<AuthBindingsContext.Provider
value={{
...authBindings,
login: handleLogin as IAuthContext["login"],
logout: handleLogout as IAuthContext["logout"],
check: handleCheck as IAuthContext["check"],
register: handleRegister as IAuthContext["register"],
forgotPassword: handleForgotPassword as IAuthContext["forgotPassword"],
updatePassword: handleUpdatePassword as IAuthContext["updatePassword"],
isProvided,
}}
>
{children}
</AuthBindingsContext.Provider>
);
};
/**
* @deprecated `useLegacyAuthContext` is deprecated with refine@4, use `useAuthBindingsContext` instead, however, we still support `useLegacyAuthContext` for backward compatibility.
*/
export const useLegacyAuthContext = () => {
const context = React.useContext(LegacyAuthContext);
return context;
};
export const useAuthBindingsContext = () => {
const context = React.useContext(AuthBindingsContext);
return context;
};

View File

@@ -0,0 +1,115 @@
/**
* @author aliemir
*
* In the current internal structure, sometimes we pass params and args from one function to another,
* like in case of `check` (formerly `checkAuth`) function, we pass the reject value to `useLogout` hook,
* which handles the redirect after logout.
*
* These actions should be separated,
*
* Apps can exist with an optional auth,
* or do not redirect after logout,
* or do the redirect but not log out,
* or do the redirect to a different page than `/login`.
*
* To cover all those cases, we should return more information from auth functions.
*
* Let's say, they should always resolve, even if user is not authenticated,
* but have the proper information to handle the situation.
*
* like `authenticated: false`, `redirect: '/login'` and `logout: true`
* which will inform refine that user is not authenticated and should be redirected to `/login` and logout.
* In some cases, redirect might need to be transferred to other hooks (like `useLogout` hook),
* but these cases can be handled internally.
*
* If the response from `check` is `{ authenticated: false, logout: false, redirect: "/not-authenticated" }`,
* then the user will be redirected to `/not-authenticated` without logging out.
*
* If the response from `check` is `{ authenticated: false, logout: true, redirect: false }`,
* then the user will be logged out without redirecting.
*
* Same goes for `onError` function, it should always resolve.
*/
import type { RefineError } from "../data/types";
export type CheckResponse = {
authenticated: boolean;
redirectTo?: string;
logout?: boolean;
error?: RefineError | Error;
};
export type OnErrorResponse = {
redirectTo?: string;
logout?: boolean;
error?: RefineError | Error;
};
export type SuccessNotificationResponse = {
message: string;
description?: string;
};
export type AuthActionResponse = {
success: boolean;
redirectTo?: string;
error?: RefineError | Error;
[key: string]: unknown;
successNotification?: SuccessNotificationResponse;
};
export type PermissionResponse = unknown;
export type IdentityResponse = unknown;
export type AuthProvider = {
login: (params: any) => Promise<AuthActionResponse>;
logout: (params: any) => Promise<AuthActionResponse>;
check: (params?: any) => Promise<CheckResponse>;
onError: (error: any) => Promise<OnErrorResponse>;
register?: (params: any) => Promise<AuthActionResponse>;
forgotPassword?: (params: any) => Promise<AuthActionResponse>;
updatePassword?: (params: any) => Promise<AuthActionResponse>;
getPermissions?: (
params?: Record<string, any>,
) => Promise<PermissionResponse>;
getIdentity?: (params?: any) => Promise<IdentityResponse>;
};
/**
* @deprecated use `AuthProvider` instead.
*/
export type AuthBindings = AuthProvider;
export interface IAuthContext extends Partial<AuthProvider> {
isProvided: boolean;
}
export type TLogoutData = void | false | string;
export type TLoginData = void | false | string | object;
export type TRegisterData = void | false | string;
export type TForgotPasswordData = void | false | string;
export type TUpdatePasswordData = void | false | string;
/**
* @deprecated `LegacyAuthProvider` is deprecated with refine@4, use `AuthProvider` instead, however, we still support `LegacyAuthProvider` for backward compatibility.
*/
export interface LegacyAuthProvider {
login: (params: any) => Promise<TLoginData>;
register?: (params: any) => Promise<TRegisterData>;
forgotPassword?: (params: any) => Promise<TForgotPasswordData>;
updatePassword?: (params: any) => Promise<TUpdatePasswordData>;
logout: (params: any) => Promise<TLogoutData>;
checkAuth: (params?: any) => Promise<any>;
checkError: (error: any) => Promise<void>;
getPermissions?: (params?: Record<string, any>) => Promise<any>;
getUserIdentity?: (params?: any) => Promise<any>;
}
/**
* @deprecated `ILegacyAuthContext` is deprecated with refine@4, use `IAuthContext` instead, however, we still support `ILegacyAuthContext` for backward compatibility.
*/
export interface ILegacyAuthContext extends Partial<LegacyAuthProvider> {
isProvided?: boolean;
}

View File

@@ -0,0 +1,40 @@
import React, { type PropsWithChildren } from "react";
import type { DataProvider, DataProviders, IDataContext } from "./types";
export const defaultDataProvider: DataProviders = {
default: {} as DataProvider,
};
export const DataContext =
React.createContext<IDataContext>(defaultDataProvider);
type Props = PropsWithChildren<{
dataProvider?: DataProvider | DataProviders;
}>;
export const DataContextProvider: React.FC<Props> = ({
children,
dataProvider,
}) => {
let providerValue = defaultDataProvider;
if (dataProvider) {
if (
!("default" in dataProvider) &&
("getList" in dataProvider || "getOne" in dataProvider)
) {
providerValue = {
default: dataProvider,
};
} else {
providerValue = dataProvider;
}
}
return (
<DataContext.Provider value={providerValue}>
{children}
</DataContext.Provider>
);
};

View File

@@ -0,0 +1,564 @@
import type { QueryFunctionContext, QueryKey } from "@tanstack/react-query";
import type { DocumentNode } from "graphql";
import type { UseListConfig } from "../../hooks/data/useList";
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
export type BaseKey = string | number;
export type BaseRecord = {
id?: BaseKey;
[key: string]: any;
};
export type BaseOption = {
label: any;
value: any;
};
/**
* @deprecated Use `BaseOption` instead.
*/
export interface Option extends BaseOption {}
export type NestedField = {
operation: string;
variables: QueryBuilderOptions[];
fields: Fields;
};
export type Fields = Array<string | object | NestedField>;
export type VariableOptions =
| {
type?: string;
name?: string;
value: any;
list?: boolean;
required?: boolean;
}
| { [k: string]: any };
export interface QueryBuilderOptions {
operation?: string;
fields?: Fields;
variables?: VariableOptions;
}
export type GraphQLQueryOptions = {
/**
* @description GraphQL query to be used by data providers.
* @optional
* @example
* ```tsx
* import gql from 'graphql-tag'
* import { useOne } from '@refinedev/core'
*
* const PRODUCT_QUERY = gql`
* query GetProduct($id: ID!) {
* product(id: $id) {
* id
* name
* }
* }
* `
*
* useOne({
* id: 1,
* meta: { gqlQuery: PRODUCT_QUERY }
* })
* ```
*/
gqlQuery?: DocumentNode;
/**
* @description GraphQL mutation to be used by data providers.
* @optional
* @example
* ```tsx
* import gql from 'graphql-tag'
* import { useCreate } from '@refinedev/core'
*
* const PRODUCT_CREATE_MUTATION = gql`
* mutation CreateProduct($input: CreateOneProductInput!) {
* createProduct(input: $input) {
* id
* name
* }
* }
* `
* const { mutate } = useCreate()
*
* mutate({
* values: { name: "My Product" },
* meta: { gqlQuery: PRODUCT_QUERY }
* })
* ```
*/
gqlMutation?: DocumentNode;
/**
* @description GraphQL Variables to be used for more advanced query filters by data providers. If filters correspond to table columns,
* these variables will not be presented in the initial filters selected and will not be reset or set by table column filtering.
* @optional
* @example
* ```tsx
* import gql from "graphql-tag";
* import { useTable } from "@refinedev/antd";
* import type { GetFieldsFromList } from "@refinedev/hasura";
* import type { GetPostsQuery } from "graphql/types";
*
* const POSTS_QUERY = gql`
* query GetPosts(
* $offset: Int!
* $limit: Int!
* $order_by: [posts_order_by!]
* $where: posts_bool_exp
* ) {
* posts(
* offset: $offset
* limit: $limit
* order_by: $order_by
* where: $where
* ) {
* id
* title
* category {
* id
* title
* }
* }
* posts_aggregate(where: $where) {
* aggregate {
* count
* }
* }
* } `;
*
*
* export const PostList = () => {
* const { tableProps } = useTable<
* GetFieldsFromList<GetPostsQuery>
* >({
* meta: {
* gqlQuery: POSTS_QUERY,
* gqlVariables: {
* where: {
* _and: [
* {
* title: {
* _ilike: "%Updated%",
* },
* },
* {
* created_at: {
* _gte: "2023-08-04T08:26:26.489116+00:00"
* }
* }
* ],
* },
* },
* }
* });
* return ( <Table {...tableProps}/>);
* }
*
* ```
*/
gqlVariables?: {
[key: string]: any;
};
};
export type MetaQuery = {
[k: string]: any;
queryContext?: Omit<QueryFunctionContext, "meta">;
} & QueryBuilderOptions &
GraphQLQueryOptions;
export interface Pagination {
/**
* Initial page index
* @default 1
*/
current?: number;
/**
* Initial number of items per page
* @default 10
*/
pageSize?: number;
/**
* Whether to use server side pagination or not.
* @default "server"
*/
mode?: "client" | "server" | "off";
}
/**
* @deprecated `MetaDataQuery` is deprecated with refine@4, use `MetaQuery` instead, however, we still support `MetaDataQuery` for backward compatibility.
*/
export type MetaDataQuery = {
[k: string]: any;
queryContext?: Omit<QueryFunctionContext, "meta">;
} & QueryBuilderOptions;
export interface IQueryKeys {
all: QueryKey;
resourceAll: QueryKey;
list: (
config?:
| UseListConfig
| {
pagination?: Required<Pagination>;
hasPagination?: boolean;
sorters?: CrudSort[];
filters?: CrudFilter[];
}
| undefined,
) => QueryKey;
many: (ids?: BaseKey[]) => QueryKey;
detail: (id?: BaseKey) => QueryKey;
logList: (meta?: Record<number | string, any>) => QueryKey;
}
export interface ValidationErrors {
[field: string]:
| string
| string[]
| boolean
| { key: string; message: string };
}
export interface HttpError extends Record<string, any> {
message: string;
statusCode: number;
errors?: ValidationErrors;
}
export type RefineError = HttpError;
export type MutationMode = "pessimistic" | "optimistic" | "undoable";
export type QueryResponse<T = BaseRecord> =
| GetListResponse<T>
| GetOneResponse<T>;
export type PreviousQuery<TData> = [QueryKey, TData | unknown];
export type PrevContext<TData> = {
previousQueries: PreviousQuery<TData>[];
/**
* @deprecated `QueryKeys` is deprecated in favor of `keys`. Please use `keys` instead to construct query keys for queries and mutations.
*/
queryKey: IQueryKeys;
};
export type Context = {
previousQueries: ContextQuery[];
};
export type ContextQuery<T = BaseRecord> = {
query: QueryResponse<T>;
queryKey: QueryKey;
};
// Filters are used as a suffix of a field name:
// | Filter | Description |
// | ------------------- | --------------------------------- |
// | `eq` | Equal |
// | ne | Not equal |
// | lt | Less than |
// | gt | Greater than |
// | lte | Less than or equal to |
// | gte | Greater than or equal to |
// | in | Included in an array |
// | nin | Not included in an array |
// | contains | Contains |
// | ncontains | Doesn't contain |
// | containss | Contains, case sensitive |
// | ncontainss | Doesn't contain, case sensitive |
// | null | Is null or not null |
// | startswith | Starts with |
// | nstartswith | Doesn't start with |
// | startswiths | Starts with, case sensitive |
// | nstartswiths | Doesn't start with, case sensitive|
// | endswith | Ends with |
// | nendswith | Doesn't end with |
// | endswiths | Ends with, case sensitive |
// | nendswiths | Doesn't end with, case sensitive |
export type CrudOperators =
| "eq"
| "ne"
| "lt"
| "gt"
| "lte"
| "gte"
| "in"
| "nin"
| "ina"
| "nina"
| "contains"
| "ncontains"
| "containss"
| "ncontainss"
| "between"
| "nbetween"
| "null"
| "nnull"
| "startswith"
| "nstartswith"
| "startswiths"
| "nstartswiths"
| "endswith"
| "nendswith"
| "endswiths"
| "nendswiths"
| "or"
| "and";
export type SortOrder = "desc" | "asc" | null;
export type LogicalFilter = {
field: string;
operator: Exclude<CrudOperators, "or" | "and">;
value: any;
};
export type ConditionalFilter = {
key?: string;
operator: Extract<CrudOperators, "or" | "and">;
value: (LogicalFilter | ConditionalFilter)[];
};
export type CrudFilter = LogicalFilter | ConditionalFilter;
export type CrudSort = {
field: string;
order: "asc" | "desc";
};
export type CrudFilters = CrudFilter[];
export type CrudSorting = CrudSort[];
export interface CustomResponse<TData = BaseRecord> {
data: TData;
}
export interface GetListResponse<TData = BaseRecord> {
data: TData[];
total: number;
[key: string]: any;
}
export interface CreateResponse<TData = BaseRecord> {
data: TData;
}
export interface CreateManyResponse<TData = BaseRecord> {
data: TData[];
}
export interface UpdateResponse<TData = BaseRecord> {
data: TData;
}
export interface UpdateManyResponse<TData = BaseRecord> {
data: TData[];
}
export interface GetOneResponse<TData = BaseRecord> {
data: TData;
}
export interface GetManyResponse<TData = BaseRecord> {
data: TData[];
}
export interface DeleteOneResponse<TData = BaseRecord> {
data: TData;
}
export interface DeleteManyResponse<TData = BaseRecord> {
data: TData[];
}
export interface GetListParams {
resource: string;
pagination?: Pagination;
/**
* @deprecated `hasPagination` is deprecated, use `pagination.mode` instead.
*/
hasPagination?: boolean;
/**
* @deprecated `sort` is deprecated, use `sorters` instead.
*/
sort?: CrudSort[];
sorters?: CrudSort[];
filters?: CrudFilter[];
meta?: MetaQuery;
/**
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
*/
metaData?: MetaQuery;
dataProviderName?: string;
}
export interface GetManyParams {
resource: string;
ids: BaseKey[];
meta?: MetaQuery;
/**
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
*/
metaData?: MetaQuery;
dataProviderName?: string;
}
export interface GetOneParams {
resource: string;
id: BaseKey;
meta?: MetaQuery;
/**
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
*/
metaData?: MetaQuery;
}
export interface CreateParams<TVariables = {}> {
resource: string;
variables: TVariables;
meta?: MetaQuery;
/**
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
*/
metaData?: MetaQuery;
}
export interface CreateManyParams<TVariables = {}> {
resource: string;
variables: TVariables[];
meta?: MetaQuery;
/**
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
*/
metaData?: MetaQuery;
}
export interface UpdateParams<TVariables = {}> {
resource: string;
id: BaseKey;
variables: TVariables;
meta?: MetaQuery;
/**
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
*/
metaData?: MetaQuery;
}
export interface UpdateManyParams<TVariables = {}> {
resource: string;
ids: BaseKey[];
variables: TVariables;
meta?: MetaQuery;
/**
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
*/
metaData?: MetaQuery;
}
export interface DeleteOneParams<TVariables = {}> {
resource: string;
id: BaseKey;
variables?: TVariables;
meta?: MetaQuery;
/**
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
*/
metaData?: MetaQuery;
}
export interface DeleteManyParams<TVariables = {}> {
resource: string;
ids: BaseKey[];
variables?: TVariables;
meta?: MetaQuery;
/**
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
*/
metaData?: MetaQuery;
}
export interface CustomParams<TQuery = unknown, TPayload = unknown> {
url: string;
method: "get" | "delete" | "head" | "options" | "post" | "put" | "patch";
/**
* @deprecated `sort` is deprecated, use `sorters` instead.
*/
sort?: CrudSort[];
sorters?: CrudSort[];
filters?: CrudFilter[];
payload?: TPayload;
query?: TQuery;
headers?: {};
meta?: MetaQuery;
/**
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
*/
metaData?: MetaQuery;
}
export type DataProvider = {
getList: <TData extends BaseRecord = BaseRecord>(
params: GetListParams,
) => Promise<GetListResponse<TData>>;
getMany?: <TData extends BaseRecord = BaseRecord>(
params: GetManyParams,
) => Promise<GetManyResponse<TData>>;
getOne: <TData extends BaseRecord = BaseRecord>(
params: GetOneParams,
) => Promise<GetOneResponse<TData>>;
create: <TData extends BaseRecord = BaseRecord, TVariables = {}>(
params: CreateParams<TVariables>,
) => Promise<CreateResponse<TData>>;
createMany?: <TData extends BaseRecord = BaseRecord, TVariables = {}>(
params: CreateManyParams<TVariables>,
) => Promise<CreateManyResponse<TData>>;
update: <TData extends BaseRecord = BaseRecord, TVariables = {}>(
params: UpdateParams<TVariables>,
) => Promise<UpdateResponse<TData>>;
updateMany?: <TData extends BaseRecord = BaseRecord, TVariables = {}>(
params: UpdateManyParams<TVariables>,
) => Promise<UpdateManyResponse<TData>>;
deleteOne: <TData extends BaseRecord = BaseRecord, TVariables = {}>(
params: DeleteOneParams<TVariables>,
) => Promise<DeleteOneResponse<TData>>;
deleteMany?: <TData extends BaseRecord = BaseRecord, TVariables = {}>(
params: DeleteManyParams<TVariables>,
) => Promise<DeleteManyResponse<TData>>;
getApiUrl: () => string;
custom?: <
TData extends BaseRecord = BaseRecord,
TQuery = unknown,
TPayload = unknown,
>(
params: CustomParams<TQuery, TPayload>,
) => Promise<CustomResponse<TData>>;
};
export type DataProviders = {
default: DataProvider;
[key: string]: DataProvider;
};
export type IDataContext = DataProviders;
export type DataBindings = DataProvider | DataProviders;

View File

@@ -0,0 +1,57 @@
import * as React from "react";
import { useGetLocale, useTranslate } from "@hooks";
import { render } from "@test";
import { I18nContextProvider } from "./";
describe("I18nContext", () => {
const TestComponent = () => {
const locale = useGetLocale();
const translate = useTranslate();
return (
<div>
<span>{`Current language: ${locale()}`}</span>
<span>
{translate("undefined key", { name: "test" }, "hello test")}
</span>
</div>
);
};
const customRender = (ui: any, providerProps?: any) => {
return render(
<I18nContextProvider {...providerProps}>{ui}</I18nContextProvider>,
providerProps,
);
};
it("should get value from I18nContext ", () => {
const providerProps = {
i18nProvider: {
translate: () => "hello",
changeLocale: () => Promise.resolve(),
getLocale: () => "tr",
},
};
const { getByText } = customRender(<TestComponent />, providerProps);
expect(getByText("Current language: tr"));
});
it("should get options value from I18nContext ", () => {
const providerProps = {
i18nProvider: {
translate: (key: string, options: any) => `hello ${options.name}`,
changeLocale: () => Promise.resolve(),
getLocale: () => "tr",
},
};
const { getByText } = customRender(<TestComponent />, providerProps);
expect(getByText("hello test")).toBeTruthy();
});
});

View File

@@ -0,0 +1,23 @@
import React, { type PropsWithChildren } from "react";
import type { I18nProvider, II18nContext } from "./types";
/** @deprecated default value for translation context has no use and is an empty object. */
export const defaultProvider: Partial<I18nProvider> = {};
export const I18nContext = React.createContext<II18nContext>({});
export const I18nContextProvider: React.FC<PropsWithChildren<II18nContext>> = ({
children,
i18nProvider,
}) => {
return (
<I18nContext.Provider
value={{
i18nProvider,
}}
>
{children}
</I18nContext.Provider>
);
};

View File

@@ -0,0 +1,22 @@
type TranslateFunction = (
key: string,
options?: any,
defaultMessage?: string,
) => string;
type ChangeLocaleFunction = (
locale: string,
options?: any,
) => Promise<any> | any;
type GetLocaleFunction = () => string;
export type I18nProvider = {
translate: TranslateFunction;
changeLocale: ChangeLocaleFunction;
getLocale: GetLocaleFunction;
};
export interface II18nContext {
i18nProvider?: I18nProvider;
}

View File

@@ -0,0 +1,16 @@
import React, { type PropsWithChildren } from "react";
import type { ILiveContext } from "./types";
export const LiveContext = React.createContext<ILiveContext>({});
export const LiveContextProvider: React.FC<PropsWithChildren<ILiveContext>> = ({
liveProvider,
children,
}) => {
return (
<LiveContext.Provider value={{ liveProvider }}>
{children}
</LiveContext.Provider>
);
};

View File

@@ -0,0 +1,113 @@
import type {
BaseKey,
CrudFilter,
CrudSort,
MetaQuery,
Pagination,
} from "../data/types";
export type LiveEvent = {
channel: string;
type: "deleted" | "updated" | "created" | "*" | string;
payload: {
ids?: BaseKey[];
[x: string]: any;
};
date: Date;
meta?: MetaQuery & {
dataProviderName?: string;
};
};
export type LiveModeProps = {
/**
* Whether to update data automatically ("auto") or not ("manual") if a related live event is received. The "off" value is used to avoid creating a subscription.
* @type [`"auto" | "manual" | "off"`](/docs/api-reference/core/providers/live-provider/#livemode)
* @default `"off"`
*/
liveMode?: "auto" | "manual" | "off";
/**
* Callback to handle all related live events of this hook.
* @type [`(event: LiveEvent) => void`](/docs/api-reference/core/interfaceReferences/#livemodeprops)
* @default `undefined`
*/
onLiveEvent?: (event: LiveEvent) => void;
/**
* Params to pass to liveProvider's subscribe method if liveMode is enabled.
* @type [`{ ids?: BaseKey[]; [key: string]: any; }`](/docs/api-reference/core/interfaceReferences/#livemodeprops)
* @default `undefined`
*/
liveParams?: {
ids?: BaseKey[];
[key: string]: any;
};
};
export type ILiveModeContextProvider = LiveModeProps;
export type LiveListParams = {
resource?: string;
pagination?: Pagination;
hasPagination?: boolean;
sorters?: CrudSort[];
filters?: CrudFilter[];
meta?: MetaQuery;
metaData?: MetaQuery;
};
export type LiveOneParams = {
resource?: string;
id?: BaseKey;
};
export type LiveManyParams = {
resource?: string;
ids?: BaseKey[];
};
export type LiveCommonParams = {
subscriptionType?: "useList" | "useOne" | "useMany";
[key: string]: unknown;
};
type LiveSubscribeOptions = {
channel: string;
types: Array<LiveEvent["type"]>;
callback: (event: LiveEvent) => void;
params?: LiveCommonParams & LiveListParams & LiveOneParams & LiveManyParams;
};
type LiveDeprecatedSubscribeOptions = {
/**
* @deprecated use `meta.dataProviderName` instead.
*/
dataProviderName?: string;
/**
* @deprecated `params.meta` is depcerated. Use `meta` directly from the root level instead.
*/
meta?: MetaQuery;
/**
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
*/
metaData?: MetaQuery;
/**
* @deprecated `hasPagination` is deprecated, use `pagination.mode` instead.
*/
hasPagination?: boolean;
/**
* @deprecated `sort` is deprecated, use `sorters` instead.
*/
sort?: CrudSort[];
};
export type LiveProvider = {
publish?: (event: LiveEvent) => void;
subscribe: (
options: LiveSubscribeOptions & LiveDeprecatedSubscribeOptions,
) => any;
unsubscribe: (subscription: any) => void;
};
export type ILiveContext = {
liveProvider?: LiveProvider;
};

View File

@@ -0,0 +1,44 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { MetaContextProvider, useMetaContext } from "./index";
import "@testing-library/jest-dom";
describe("MetaContextProvider", () => {
it("provides the correct meta context value to child components", () => {
const TestComponent = () => {
const meta = useMetaContext();
return <div>{meta.testKey}</div>;
};
render(
<MetaContextProvider value={{ testKey: "testValue" }}>
<TestComponent />
</MetaContextProvider>,
);
expect(screen.getByText("testValue")).toBeInTheDocument();
});
it("merges existing context value with new value", () => {
const TestComponent = () => {
const meta = useMetaContext();
return (
<>
<div>{meta.firstKey}</div>
<div>{meta.secondKey}</div>
</>
);
};
render(
<MetaContextProvider value={{ firstKey: "value1" }}>
<MetaContextProvider value={{ secondKey: "value2" }}>
<TestComponent />
</MetaContextProvider>
</MetaContextProvider>,
);
expect(screen.getByText("value1")).toBeInTheDocument();
expect(screen.getByText("value2")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,45 @@
import React, {
type ReactNode,
createContext,
useContext,
useMemo,
} from "react";
type MetaContextValue = Record<string, any>;
export const MetaContext = createContext<MetaContextValue>({});
/**
* Is used to provide meta data to the children components.
* @internal
*/
export const MetaContextProvider = ({
children,
value,
}: { children: ReactNode; value: MetaContextValue }) => {
const currentValue = useMetaContext();
const metaContext = useMemo(() => {
return {
...currentValue,
...value,
};
}, [currentValue, value]);
return (
<MetaContext.Provider value={metaContext}>{children}</MetaContext.Provider>
);
};
/**
* @internal
* @returns The MetaContext value.
*/
export const useMetaContext = () => {
const context = useContext(MetaContext);
if (!context) {
throw new Error("useMetaContext must be used within a MetaContextProvider");
}
return useContext(MetaContext);
};

View File

@@ -0,0 +1,18 @@
import React, { createContext, type PropsWithChildren } from "react";
import type { INotificationContext } from "./types";
/** @deprecated default value for notification context has no use and is an empty object. */
export const defaultNotificationProvider: INotificationContext = {};
export const NotificationContext = createContext<INotificationContext>({});
export const NotificationContextProvider: React.FC<
PropsWithChildren<INotificationContext>
> = ({ open, close, children }) => {
return (
<NotificationContext.Provider value={{ open, close }}>
{children}
</NotificationContext.Provider>
);
};

View File

@@ -0,0 +1,47 @@
export type SuccessErrorNotification<
TData = unknown,
TError = unknown,
TVariables = unknown,
> = {
/**
* Success notification configuration to be displayed when the mutation is successful.
* @default '"There was an error creating resource (status code: `statusCode`)" or "Error when updating resource (status code: statusCode)"'
*/
successNotification?:
| OpenNotificationParams
| false
| ((
data?: TData,
values?: TVariables,
resource?: string,
) => OpenNotificationParams | false | undefined);
/**
* Error notification configuration to be displayed when the mutation fails.
* @default '"There was an error creating resource (status code: `statusCode`)" or "Error when updating resource (status code: statusCode)"'
*/
errorNotification?:
| OpenNotificationParams
| false
| ((
error?: TError,
values?: TVariables,
resource?: string,
) => OpenNotificationParams | false | undefined);
};
export type OpenNotificationParams = {
key?: string;
message: string;
type: "success" | "error" | "progress";
description?: string;
cancelMutation?: () => void;
undoableTimeout?: number;
};
export interface INotificationContext {
open?: (params: OpenNotificationParams) => void;
close?: (key: string) => void;
}
export type NotificationProvider = Required<INotificationContext>;

View File

@@ -0,0 +1,128 @@
import React from "react";
import pluralize from "pluralize";
import { DefaultLayout } from "@components/layoutWrapper/defaultLayout";
import { humanizeString } from "../../definitions/helpers/humanizeString";
import type {
IRefineContext,
IRefineContextOptions,
IRefineContextProvider,
} from "./types";
import { LoginPage as DefaultLoginPage } from "@components/pages";
const defaultTitle: IRefineContextOptions["title"] = {
icon: (
<svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
data-testid="refine-logo"
id="refine-default-logo"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.7889 0.422291C12.6627 -0.140764 11.3373 -0.140764 10.2111 0.422291L2.21115 4.42229C0.85601 5.09986 0 6.48491 0 8V16C0 17.5151 0.85601 18.9001 2.21115 19.5777L10.2111 23.5777C11.3373 24.1408 12.6627 24.1408 13.7889 23.5777L21.7889 19.5777C23.144 18.9001 24 17.5151 24 16V8C24 6.48491 23.144 5.09986 21.7889 4.42229L13.7889 0.422291ZM8 8C8 5.79086 9.79086 4 12 4C14.2091 4 16 5.79086 16 8V16C16 18.2091 14.2091 20 12 20C9.79086 20 8 18.2091 8 16V8Z"
fill="currentColor"
/>
<path
d="M14 8C14 9.10457 13.1046 10 12 10C10.8954 10 10 9.10457 10 8C10 6.89543 10.8954 6 12 6C13.1046 6 14 6.89543 14 8Z"
fill="currentColor"
/>
</svg>
),
text: "Refine Project",
};
export const defaultRefineOptions: IRefineContextOptions = {
mutationMode: "pessimistic",
syncWithLocation: false,
undoableTimeout: 5000,
warnWhenUnsavedChanges: false,
liveMode: "off",
redirect: {
afterCreate: "list",
afterClone: "list",
afterEdit: "list",
},
overtime: {
interval: 1000,
},
textTransformers: {
humanize: humanizeString,
plural: pluralize.plural,
singular: pluralize.singular,
},
disableServerSideValidation: false,
title: defaultTitle,
};
export const RefineContext = React.createContext<IRefineContext>({
hasDashboard: false,
mutationMode: "pessimistic",
warnWhenUnsavedChanges: false,
syncWithLocation: false,
undoableTimeout: 5000,
Title: undefined,
Sider: undefined,
Header: undefined,
Footer: undefined,
Layout: DefaultLayout,
OffLayoutArea: undefined,
liveMode: "off",
onLiveEvent: undefined,
options: defaultRefineOptions,
});
export const RefineContextProvider: React.FC<IRefineContextProvider> = ({
hasDashboard,
mutationMode,
warnWhenUnsavedChanges,
syncWithLocation,
undoableTimeout,
children,
DashboardPage,
Title,
Layout = DefaultLayout,
Header,
Sider,
Footer,
OffLayoutArea,
LoginPage = DefaultLoginPage,
catchAll,
liveMode = "off",
onLiveEvent,
options,
}) => {
return (
<RefineContext.Provider
value={{
__initialized: true,
hasDashboard,
mutationMode,
warnWhenUnsavedChanges,
syncWithLocation,
Title,
undoableTimeout,
Layout,
Header,
Sider,
Footer,
OffLayoutArea,
DashboardPage,
LoginPage,
catchAll,
liveMode,
onLiveEvent,
options,
}}
>
{children}
</RefineContext.Provider>
);
};

View File

@@ -0,0 +1,335 @@
import React, { type ReactNode } from "react";
import type { QueryClient, QueryClientConfig } from "@tanstack/react-query";
import type { RedirectAction } from "../../hooks/form/types";
import type { UseLoadingOvertimeRefineContext } from "../../hooks/useLoadingOvertime";
import type { AccessControlProvider } from "../accessControl/types";
import type { AuditLogProvider } from "../auditLog/types";
import type { AuthProvider, LegacyAuthProvider } from "../auth/types";
import type { DataProvider, DataProviders, MutationMode } from "../data/types";
import type { I18nProvider } from "../i18n/types";
import type { LiveModeProps, LiveProvider } from "../live/types";
import type { NotificationProvider } from "../notification/types";
import type { ResourceProps } from "../resource/types";
import type { LegacyRouterProvider } from "../router/legacy/types";
import type { RouterProvider } from "../router/types";
export type TitleProps = {
collapsed: boolean;
};
export type LayoutProps = {
Sider?: React.FC<{
Title?: React.FC<TitleProps>;
render?: (props: {
items: JSX.Element[];
logout: React.ReactNode;
dashboard: React.ReactNode;
collapsed: boolean;
}) => React.ReactNode;
meta?: Record<string, unknown>;
}>;
Header?: React.FC;
Title?: React.FC<TitleProps>;
Footer?: React.FC;
OffLayoutArea?: React.FC;
dashboard?: boolean;
children?: ReactNode;
};
export type DashboardPageProps<TCrudData = any> = {
initialData?: TCrudData;
} & Record<any, any>;
export type TextTransformers = {
/**
* Convert a camelized/dasherized/underscored string into a humanized one
* @example
* humanize("some_name") => "Some name"
*/
humanize?: (text: string) => string;
/**
* Pluralize a word
* @example
* plural('regex') => "regexes"
*/
plural?: (word: string) => string;
/**
* Singularize a word
* @example
* singular('singles') => "single"
*/
singular?: (word: string) => string;
};
export interface IRefineOptions {
breadcrumb?: ReactNode;
mutationMode?: MutationMode;
syncWithLocation?: boolean;
warnWhenUnsavedChanges?: boolean;
undoableTimeout?: number;
liveMode?: LiveModeProps["liveMode"];
disableTelemetry?: boolean;
redirect?: {
afterCreate?: RedirectAction;
afterClone?: RedirectAction;
afterEdit?: RedirectAction;
};
reactQuery?: {
clientConfig?: QueryClientConfig | InstanceType<typeof QueryClient>;
/**
* @deprecated `@tanstack/react-query`'s devtools are removed from the core. Please use the `@tanstack/react-query-devtools` package manually in your project. This option will be removed in the next major version and has no effect on the `@tanstack/react-query-devtools` package usage.
*/
devtoolConfig?: any | false;
};
overtime?: UseLoadingOvertimeRefineContext;
textTransformers?: TextTransformers;
/**
* Disables server-side validation globally for the useForm hook
* @default false
* @see {@link https://refine.dev/docs/advanced-tutorials/forms/server-side-form-validation/}
*/
disableServerSideValidation?: boolean;
/**
* The project id of your refine project. Will be set automatically. Don't modify.
*/
projectId?: string;
useNewQueryKeys?: boolean;
/**
* Icon and name for the app title. These values are used as default values in the <ThemedLayoutV2 /> and <AuthPage /> components.
* By default, `icon` is the Refine logo and `text` is "Refine Project".
*/
title?: {
icon?: React.ReactNode;
text?: React.ReactNode;
};
}
export interface IRefineContextOptions {
breadcrumb?: ReactNode;
mutationMode: MutationMode;
syncWithLocation: boolean;
warnWhenUnsavedChanges: boolean;
undoableTimeout: number;
liveMode: LiveModeProps["liveMode"];
redirect: {
afterCreate: RedirectAction;
afterClone: RedirectAction;
afterEdit: RedirectAction;
};
overtime: UseLoadingOvertimeRefineContext;
textTransformers: Required<TextTransformers>;
disableServerSideValidation: boolean;
projectId?: string;
useNewQueryKeys?: boolean;
title: {
icon?: React.ReactNode;
text?: React.ReactNode;
};
}
export interface IRefineContext {
__initialized?: boolean;
hasDashboard: boolean;
mutationMode: MutationMode;
/**
* @deprecated Please use `UnsavedChangesNotifier` components from router packages instead.
*/
warnWhenUnsavedChanges: boolean;
syncWithLocation: boolean;
undoableTimeout: number;
catchAll?: React.ReactNode;
DashboardPage?: RefineProps["DashboardPage"];
LoginPage?: React.FC | false;
Title?: React.FC<TitleProps>;
Layout: React.FC<LayoutProps>;
Sider?: React.FC;
Header?: React.FC;
Footer?: React.FC;
OffLayoutArea?: React.FC;
liveMode: LiveModeProps["liveMode"];
onLiveEvent?: LiveModeProps["onLiveEvent"];
options: IRefineContextOptions;
}
export interface IRefineContextProvider {
__initialized?: boolean;
hasDashboard: boolean;
mutationMode: MutationMode;
warnWhenUnsavedChanges: boolean;
syncWithLocation: boolean;
undoableTimeout: number;
/**
* @deprecated Please use the `catchAll` element in your routes instead.
*/
catchAll?: React.ReactNode;
/**
* @deprecated Please use the `DashboardPage` component in your routes instead.
*/
DashboardPage?: RefineProps["DashboardPage"];
/**
* @deprecated Please use the `LoginPage` component in your routes instead.
*/
LoginPage?: React.FC | false;
/**
* @deprecated Please pass the `Title` component to your `Layout` component.
*/
Title?: React.FC<TitleProps>;
/**
* @deprecated Please use the `Layout` component as a children instead of a prop.
*/
Layout?: React.FC<LayoutProps>;
/**
* @deprecated Please pass the `Sider` component to your `Layout` component.
*/
Sider?: React.FC;
/**
* @deprecated Please pass the `Header` component to your `Layout` component.
*/
Header?: React.FC;
/**
* @deprecated Please pass the `Footer` component to your `Layout` component.
*/
Footer?: React.FC;
/**
* @deprecated Please use your `OffLayoutArea` component as a children instead of a prop.
*/
OffLayoutArea?: React.FC;
liveMode: LiveModeProps["liveMode"];
onLiveEvent?: LiveModeProps["onLiveEvent"];
options: IRefineContextOptions;
children?: ReactNode;
}
export interface RefineProps {
children?: React.ReactNode;
/**
* `resources` is the predefined interaction points for a refine app. A resource represents an entity in an endpoint in the API.
* While this is not a required property, it is used in resource detection and creation of routes for the app.
* @type [`ResourceProps[]`](https://refine.dev/docs/api-reference/core/components/refine-config/#resources)
*/
resources?: ResourceProps[];
/**
* **refine** needs some router functions to create resource pages, handle navigation, etc. This provider allows you to use the router library you want
* @type [`IRouterProvider`](https://refine.dev/docs/api-reference/core/providers/router-provider/)
* @deprecated This property is deprecated and was the legacy way of routing. Please use `routerProvider` with new router bindings instead.
*/
legacyRouterProvider?: LegacyRouterProvider;
/**
* Router bindings for **refine**. A simple interface for **refine** to interact with your router in a flexible way.
* @type [`RouterProvider`](https://refine.dev/docs/routing/router-provider/)
*/
routerProvider?: RouterProvider;
/**
* A `dataProvider` is the place where a refine app communicates with an API. Data providers also act as adapters for refine, making it possible for it to consume different API's and data services.
* @type [`DataProvider` | `DataProviders`](https://refine.dev/docs/api-reference/core/providers/data-provider/)
*/
dataProvider?: DataProvider | DataProviders;
/**
* `authProvider` handles authentication logic like login, logout flow and checking user credentials. It is an object with methods that refine uses when necessary.
* @type [`AuthProvider`](https://refine.dev/docs/api-reference/core/providers/auth-provider/)
*/
authProvider?: AuthProvider;
/**
* `legacyAuthProvider` handles authentication logic like login, logout flow and checking user credentials. It is an object with methods that refine uses when necessary.
* @type [`AuthProvider`](https://refine.dev/docs/api-reference/core/providers/auth-provider/)
* @deprecated `legacyAuthProvider` is deprecated with refine@4, use `authProvider` instead.
*/
legacyAuthProvider?: LegacyAuthProvider;
/**
* **refine** lets you add Realtime support to your app via `liveProvider`. It can be used to update and show data in Realtime throughout your app.
* @type [`LiveProvider`](https://refine.dev/docs/api-reference/core/providers/live-provider/)
*/
liveProvider?: LiveProvider;
/**
* `notificationProvider` handles notification logics. It is an object with methods that refine uses when necessary.
* @type [`NotificationProvider` | `(() => NotificationProvider)`](https://refine.dev/docs/api-reference/core/providers/notification-provider/)
*/
notificationProvider?: NotificationProvider | (() => NotificationProvider);
/**
* `accessControlProvider` is the entry point for implementing access control for refine apps.
* @type [`AccessControlProvider`](https://refine.dev/docs/api-reference/core/providers/accessControl-provider/)
*/
accessControlProvider?: AccessControlProvider;
/**
* **refine** allows you to track changes in your data and keep track of who made the changes.
* @type [`AuditLogProvider`](https://refine.dev/docs/api-reference/core/providers/audit-log-provider#overview)
*/
auditLogProvider?: AuditLogProvider;
/**
* `i18nProvider` property lets you add i18n support to your app. Making you able to use any i18n framework.
* @type [`i18nProvider`](https://refine.dev/docs/api-reference/core/providers/i18n-provider/)
*/
i18nProvider?: I18nProvider;
/**
* A custom error component.
* @type [`ReactNode`](https://refine.dev/docs/api-reference/core/components/refine-config/#catchall)
* @deprecated Please use the `catchAll` element in your routes instead.
*/
catchAll?: React.ReactNode;
/**
* Custom login component can be passed to the `LoginPage` property.
* @type [`React.FC`](https://refine.dev/docs/api-reference/core/components/refine-config/#loginpage)
* @deprecated Please use the `LoginPage` component in your routes instead.
*/
LoginPage?: React.FC;
/**
* A custom dashboard page can be passed to the `DashboardPage` prop which is accessible on root route.
* @type [`React.FC<DashboardPageProps>`](https://refine.dev/docs/api-reference/core/components/refine-config/#dashboardpage)
* @deprecated Please use the `DashboardPage` component in your routes instead.
*/
DashboardPage?: React.FC<DashboardPageProps>;
/**
* Custom ready page component can be set by passing to `ReadyPage` property.
* @type [`React.FC`](https://refine.dev/docs/api-reference/core/components/refine-config/#readypage)
* @deprecated This component is only used with the legacy router and will be removed in the future.
*/
ReadyPage?: React.FC;
/**
* Default layout can be customized by passing the `Layout` property.
* @type [`React.FC<LayoutProps>`](https://refine.dev/docs/api-reference/core/components/refine-config/#layout)
* @deprecated Please use the `Layout` component as a children instead of a prop.
*/
Layout?: React.FC<LayoutProps>;
/**
* The default sidebar can be customized by using refine hooks and passing custom components to `Sider` property.
* @type [`React.FC`](https://refine.dev/docs/api-reference/core/components/refine-config/#sider)
* @deprecated Please pass the `Sider` component to your `Layout` component.
*/
Sider?: React.FC;
/**
* The default app header can be customized by passing the `Header` property.
* @type [`React.FC`](https://refine.dev/docs/api-reference/core/components/refine-config/#header)
* @deprecated Please pass the `Header` component to your `Layout` component.
*/
Header?: React.FC;
/**
*The default app footer can be customized by passing the `Footer` property.
* @type [`React.FC`](https://refine.dev/docs/api-reference/core/components/refine-config/#footer)
* @deprecated Please pass the `Footer` component to your `Layout` component.
*/
Footer?: React.FC;
/**
* The component wanted to be placed out of app layout structure can be set by passing to `OffLayoutArea` prop.
* @type [`React.FC`](https://refine.dev/docs/api-reference/core/components/refine-config/#offlayoutarea)
* @deprecated Please use your `OffLayoutArea` component as a children instead of a prop.
*/
OffLayoutArea?: React.FC;
/**
* TThe app title can be set by passing the `Title` property.
* @type [`React.FC<TitleProps>`](https://refine.dev/docs/api-reference/core/components/refine-config/#title)
* @deprecated Please pass the `Title` component to your `Layout` component.
*/
Title?: React.FC<TitleProps>;
/**
* Callback to handle all live events.
* @type [`(event: LiveEvent) => void`](https://refine.dev/docs/api-reference/core/providers/live-provider/#onliveevent)
*/
onLiveEvent?: LiveModeProps["onLiveEvent"];
/**
* `options` is used to configure the app.
* @type [`IRefineOptions`](https://refine.dev/docs/api-reference/core/components/refine-config/#options)
* */
options?: IRefineOptions;
}

View File

@@ -0,0 +1,24 @@
import React from "react";
import { legacyResourceTransform } from "@definitions/helpers";
import { useDeepMemo } from "@hooks/deepMemo";
import type { IResourceContext, IResourceItem, ResourceProps } from "./types";
export const ResourceContext = React.createContext<IResourceContext>({
resources: [],
});
export const ResourceContextProvider: React.FC<
React.PropsWithChildren<{ resources: ResourceProps[] }>
> = ({ resources: providedResources, children }) => {
const resources: IResourceItem[] = useDeepMemo(() => {
return legacyResourceTransform(providedResources ?? []);
}, [providedResources]);
return (
<ResourceContext.Provider value={{ resources }}>
{children}
</ResourceContext.Provider>
);
};

View File

@@ -0,0 +1,205 @@
import type { ComponentType, ReactNode } from "react";
import type { UseQueryResult } from "@tanstack/react-query";
import type { ILogData } from "../auditLog/types";
/**
* Resource route components
*/
export type ResourceRouteComponent = ComponentType<
IResourceComponentsProps<any, any>
>;
export type ResourceRoutePath = string;
export type ResourceRouteDefinition = {
path: ResourceRoutePath;
component: ResourceRouteComponent;
};
export type ResourceRouteComposition =
| ResourceRouteDefinition
| ResourceRoutePath
| ResourceRouteComponent;
export interface IResourceComponents {
list?: ResourceRouteComposition;
create?: ResourceRouteComposition;
clone?: ResourceRouteComposition;
edit?: ResourceRouteComposition;
show?: ResourceRouteComposition;
}
export type AnyString = string & { __ignore?: never };
export type ResourceAuditLogPermissions =
| "create"
| "update"
| "delete"
| AnyString;
/** 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;
}
export interface ResourceMeta extends KnownResourceMeta {
[key: string]: any;
}
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;
/**
* @deprecated Please use the `meta.parent` property instead
*/
parentName?: string;
}
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;
}
export interface IResourceComponentsProps<
TCrudData = any,
TLogQueryResult = ILogData,
> extends RouteableProperties {
name?: string;
initialData?: TCrudData;
options?: ResourceMeta & DeprecatedOptions;
logQueryResult?: UseQueryResult<TLogQueryResult>;
}
export interface IResourceItem
extends IResourceComponents,
RouteableProperties,
ResourceProps {
/**
* @deprecated Please use the `meta.label` property instead.
*/
label?: string;
/**
* @deprecated Please use action components and `getDefaultActionPath` helper instead.
*/
route?: string;
}
export interface IResourceContext {
resources: IResourceItem[];
}
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[];
}
/* Backward compatible version of 'TreeMenuItem' */
export type ITreeMenu = IResourceItem & {
key?: string;
children: ITreeMenu[];
};
export type IMenuItem = IResourceItem & {
key: string;
route: string;
};

View File

@@ -0,0 +1,18 @@
import React, { createContext, type PropsWithChildren } from "react";
import type { RouterProvider } from "./types";
const defaultRouterProvider = {};
export const RouterContext = createContext<RouterProvider>(
defaultRouterProvider,
);
export const RouterContextProvider: React.FC<
PropsWithChildren<{ router?: RouterProvider }>
> = ({ children, router }) => {
return (
<RouterContext.Provider value={router ?? defaultRouterProvider}>
{children}
</RouterContext.Provider>
);
};

View File

@@ -0,0 +1,41 @@
import React, { type PropsWithChildren } from "react";
import type { ILegacyRouterContext } from "./types";
export const defaultProvider: ILegacyRouterContext = {
useHistory: () => false,
useLocation: () => false,
useParams: () => ({}) as any,
Prompt: () => null,
Link: () => null,
};
export const LegacyRouterContext =
React.createContext<ILegacyRouterContext>(defaultProvider);
export const LegacyRouterContextProvider: React.FC<
PropsWithChildren<Partial<ILegacyRouterContext>>
> = ({
children,
useHistory,
useLocation,
useParams,
Prompt,
Link,
routes,
}) => {
return (
<LegacyRouterContext.Provider
value={{
useHistory: useHistory ?? defaultProvider.useHistory,
useLocation: useLocation ?? defaultProvider.useLocation,
useParams: useParams ?? defaultProvider.useParams,
Prompt: Prompt ?? defaultProvider.Prompt,
Link: Link ?? defaultProvider.Link,
routes: routes ?? defaultProvider.routes,
}}
>
{children}
</LegacyRouterContext.Provider>
);
};

View File

@@ -0,0 +1,50 @@
import React from "react";
import type { Action } from "../types";
export interface LegacyRouterProvider {
useHistory: () => {
push: (...args: any) => any;
replace: (...args: any) => any;
goBack: (...args: any) => any;
};
useLocation: () => {
search: string;
pathname: string;
};
useParams: <Params extends { [K in keyof Params]?: string } = {}>() => Params;
Prompt: React.FC<PromptProps>;
Link: React.FC<any>;
RouterComponent?: React.FC<any>;
routes?: any;
}
export interface ILegacyRouterContext {
useHistory: () => any;
useLocation: () => any;
useParams: <Params extends { [K in keyof Params]?: string } = {}>() => Params;
Prompt: React.FC<PromptProps>;
Link: React.FC<any>;
routes?: any;
}
export type PromptProps = {
message: string;
when?: boolean;
setWarnWhen?: (warnWhen: boolean) => void;
};
export type RouteAction = Exclude<Action, "list"> | undefined;
export type ActionWithPage = Extract<Action, "show" | "create" | "edit">;
export type ResourceRouterParams = {
resource: string;
id?: string;
action: RouteAction;
};
export type ResourceErrorRouterParams = {
resource: string;
action: ActionWithPage | undefined;
};

View File

@@ -0,0 +1,21 @@
import React from "react";
/**
* This context is used to determine which router to use.
*
* This is a temporary solution until we remove the legacy router.
*/
export const RouterPickerContext = React.createContext<"legacy" | "new">("new");
export const RouterPickerProvider = RouterPickerContext.Provider;
/**
* This is a temporary hook to determine which router to use.
* It will be removed once the legacy router is removed.
* @internal This is an internal hook.
*/
export const useRouterType = () => {
const value = React.useContext(RouterPickerContext);
return value;
};

View File

@@ -0,0 +1,79 @@
/**
* @author aliemir
*
* Router bindings interface, used to define the router bindings of refine.
*
* We're marking of the functions as optional, some features may not work properly but this is intentional.
* Users can choose to use the router bindings or not, or use their own router bindings.
* Leaving the control to the user is the best way to go.
*
* We're defining the functions as function generators, this is to allow the user to use hooks inside the functions.
*
* `go` function is used to navigate to a specific route. We're expecting a `GoConfig` object as the only parameter.
* Passing `query` as an object, will also let users to stringify the object as they like or ignore it completely or even use a custom logic to handle query strings.
*
* `back` function is used to navigate back to the previous route. It doesn't take any parameters.
* This one is a basic function for the back buttons, absence of this function can also hide the back button,
* but this depends on the UI package implementations.
*
* `parse` function is used to parse the current route, query parameters and other information.
* We're expecting this function to lead refine to the correct resource, action and id (again, not required but recommended).
* Also there's `params` property, which is used in data hooks and other places.
* This property has an interface to match but not restricted to it.
*
* Instead of a single `useNavigation` hook,
* we can separate those functions into three different hooks,
* `useGo`, `useBack` and `useParsed`
*/
import type { BaseKey, CrudFilter, CrudSort } from "../data/types";
import type { IResourceItem } from "../resource/types";
export type Action = "create" | "edit" | "list" | "show" | "clone";
export type GoConfig = {
to?: string;
query?: Record<string, unknown>;
hash?: string;
options?: {
keepQuery?: boolean;
keepHash?: boolean;
};
type?: "push" | "replace" | "path";
};
export type ParsedParams<
TParams extends Record<string, any> = Record<string, any>,
> = {
filters?: CrudFilter[];
sorters?: CrudSort[];
current?: number;
pageSize?: number;
} & TParams;
export type ParseResponse<
TParams extends Record<string, any> = Record<string, any>,
> = {
params?: ParsedParams<TParams>;
resource?: IResourceItem;
id?: BaseKey;
action?: Action;
pathname?: string;
};
export type GoFunction = (config: GoConfig) => void | string;
export type BackFunction = () => void;
export type ParseFunction<
TParams extends Record<string, any> = Record<string, any>,
> = () => ParseResponse<TParams>;
export type RouterProvider = {
go?: () => GoFunction;
back?: () => BackFunction;
parse?: () => ParseFunction;
Link?: React.ComponentType<
React.PropsWithChildren<{ to: string; [prop: string]: any }>
>;
};

View File

@@ -0,0 +1,88 @@
import * as React from "react";
import { renderHook } from "@testing-library/react";
import { act } from "@test";
import { undoableQueueReducer } from ".";
describe("Notification Reducer", () => {
const notificationDispatch = jest.fn();
const providerProps = {
notifications: [
{
id: "1",
resource: "posts",
seconds: 5000,
isRunning: true,
},
],
notificationDispatch: notificationDispatch,
};
it("should render notification item with ADD action", () => {
const { result } = renderHook(() =>
React.useReducer(undoableQueueReducer, []),
);
const [, dispatch] = result.current;
act(() => {
dispatch({ type: "ADD", payload: providerProps.notifications[0] });
});
const [state] = result.current;
expect(state).toEqual([
{
id: "1",
resource: "posts",
seconds: 5000,
isRunning: true,
},
]);
});
it("remove notification item with DELETE action", async () => {
const { result } = renderHook(() =>
React.useReducer(undoableQueueReducer, providerProps.notifications),
);
const [, dispatch] = result.current;
act(() => {
dispatch({
type: "REMOVE",
payload: providerProps.notifications[0],
});
});
const [state] = result.current;
expect(state).toEqual([]);
});
it("decrease notification item by 1 second with DECREASE_NOTIFICATION_SECOND action", async () => {
const { result } = renderHook(() =>
React.useReducer(undoableQueueReducer, providerProps.notifications),
);
const [, dispatch] = result.current;
act(() => {
dispatch({
type: "DECREASE_NOTIFICATION_SECOND",
payload: {
id: providerProps.notifications[0].id,
seconds: providerProps.notifications[0].seconds,
resource: providerProps.notifications[0].resource,
},
});
});
const [state] = result.current;
expect(state[0].seconds).toEqual(
providerProps.notifications[0].seconds - 1000,
);
});
});

View File

@@ -0,0 +1,90 @@
import React, {
createContext,
useReducer,
type PropsWithChildren,
} from "react";
import isEqual from "lodash/isEqual";
import { UndoableQueue } from "../../components";
import {
ActionTypes,
type IUndoableQueue,
type IUndoableQueueContext,
} from "./types";
export const UndoableQueueContext = createContext<IUndoableQueueContext>({
notifications: [],
notificationDispatch: () => false,
});
const initialState: IUndoableQueue[] = [];
export const undoableQueueReducer = (state: IUndoableQueue[], action: any) => {
switch (action.type) {
case ActionTypes.ADD: {
const newState = state.filter((notificationItem: IUndoableQueue) => {
return !(
isEqual(notificationItem.id, action.payload.id) &&
notificationItem.resource === action.payload.resource
);
});
return [
...newState,
{
...action.payload,
isRunning: true,
},
];
}
case ActionTypes.REMOVE:
return state.filter(
(notificationItem: IUndoableQueue) =>
!(
isEqual(notificationItem.id, action.payload.id) &&
notificationItem.resource === action.payload.resource
),
);
case ActionTypes.DECREASE_NOTIFICATION_SECOND:
return state.map((notificationItem: IUndoableQueue) => {
if (
isEqual(notificationItem.id, action.payload.id) &&
notificationItem.resource === action.payload.resource
) {
return {
...notificationItem,
seconds: action.payload.seconds - 1000,
};
}
return notificationItem;
});
default:
return state;
}
};
export const UndoableQueueContextProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
const [notifications, notificationDispatch] = useReducer(
undoableQueueReducer,
initialState,
);
const notificationData = { notifications, notificationDispatch };
return (
<UndoableQueueContext.Provider value={notificationData}>
{children}
{typeof window !== "undefined"
? notifications.map((notification) => (
<UndoableQueue
key={`${notification.id}-${notification.resource}-queue`}
notification={notification}
/>
))
: null}
</UndoableQueueContext.Provider>
);
};

View File

@@ -0,0 +1,22 @@
import type { BaseKey } from "../data/types";
export enum ActionTypes {
ADD = "ADD",
REMOVE = "REMOVE",
DECREASE_NOTIFICATION_SECOND = "DECREASE_NOTIFICATION_SECOND",
}
export interface IUndoableQueue {
id: BaseKey;
resource: string;
cancelMutation: () => void;
doMutation: () => void;
seconds: number;
isRunning: boolean;
isSilent: boolean;
}
export interface IUndoableQueueContext {
notifications: IUndoableQueue[];
notificationDispatch: React.Dispatch<any>;
}

View File

@@ -0,0 +1,17 @@
import React, { useState, type PropsWithChildren } from "react";
import type { IUnsavedWarnContext } from "./types";
export const UnsavedWarnContext = React.createContext<IUnsavedWarnContext>({});
export const UnsavedWarnContextProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
const [warnWhen, setWarnWhen] = useState(false);
return (
<UnsavedWarnContext.Provider value={{ warnWhen, setWarnWhen }}>
{children}
</UnsavedWarnContext.Provider>
);
};

View File

@@ -0,0 +1,4 @@
export interface IUnsavedWarnContext {
warnWhen?: boolean;
setWarnWhen?: (value: boolean) => void;
}