mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
packages
This commit is contained in:
46
packages/core/src/contexts/accessControl/index.tsx
Normal file
46
packages/core/src/contexts/accessControl/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
93
packages/core/src/contexts/accessControl/types.ts
Normal file
93
packages/core/src/contexts/accessControl/types.ts
Normal 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;
|
||||
};
|
||||
15
packages/core/src/contexts/auditLog/index.tsx
Normal file
15
packages/core/src/contexts/auditLog/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
45
packages/core/src/contexts/auditLog/types.ts
Normal file
45
packages/core/src/contexts/auditLog/types.ts
Normal 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>;
|
||||
200
packages/core/src/contexts/auth/index.tsx
Normal file
200
packages/core/src/contexts/auth/index.tsx
Normal 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;
|
||||
};
|
||||
115
packages/core/src/contexts/auth/types.ts
Normal file
115
packages/core/src/contexts/auth/types.ts
Normal 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;
|
||||
}
|
||||
40
packages/core/src/contexts/data/index.tsx
Normal file
40
packages/core/src/contexts/data/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
564
packages/core/src/contexts/data/types.ts
Normal file
564
packages/core/src/contexts/data/types.ts
Normal 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;
|
||||
57
packages/core/src/contexts/i18n/index.spec.tsx
Normal file
57
packages/core/src/contexts/i18n/index.spec.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
23
packages/core/src/contexts/i18n/index.tsx
Normal file
23
packages/core/src/contexts/i18n/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
22
packages/core/src/contexts/i18n/types.ts
Normal file
22
packages/core/src/contexts/i18n/types.ts
Normal 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;
|
||||
}
|
||||
16
packages/core/src/contexts/live/index.tsx
Normal file
16
packages/core/src/contexts/live/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
113
packages/core/src/contexts/live/types.ts
Normal file
113
packages/core/src/contexts/live/types.ts
Normal 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;
|
||||
};
|
||||
44
packages/core/src/contexts/metaContext/index.spec.tsx
Normal file
44
packages/core/src/contexts/metaContext/index.spec.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
45
packages/core/src/contexts/metaContext/index.tsx
Normal file
45
packages/core/src/contexts/metaContext/index.tsx
Normal 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);
|
||||
};
|
||||
18
packages/core/src/contexts/notification/index.tsx
Normal file
18
packages/core/src/contexts/notification/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
47
packages/core/src/contexts/notification/types.ts
Normal file
47
packages/core/src/contexts/notification/types.ts
Normal 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>;
|
||||
128
packages/core/src/contexts/refine/index.tsx
Normal file
128
packages/core/src/contexts/refine/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
335
packages/core/src/contexts/refine/types.ts
Normal file
335
packages/core/src/contexts/refine/types.ts
Normal 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;
|
||||
}
|
||||
24
packages/core/src/contexts/resource/index.tsx
Normal file
24
packages/core/src/contexts/resource/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
205
packages/core/src/contexts/resource/types.ts
Normal file
205
packages/core/src/contexts/resource/types.ts
Normal 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;
|
||||
};
|
||||
18
packages/core/src/contexts/router/index.tsx
Normal file
18
packages/core/src/contexts/router/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
41
packages/core/src/contexts/router/legacy/index.tsx
Normal file
41
packages/core/src/contexts/router/legacy/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
50
packages/core/src/contexts/router/legacy/types.ts
Normal file
50
packages/core/src/contexts/router/legacy/types.ts
Normal 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;
|
||||
};
|
||||
21
packages/core/src/contexts/router/picker/index.tsx
Normal file
21
packages/core/src/contexts/router/picker/index.tsx
Normal 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;
|
||||
};
|
||||
79
packages/core/src/contexts/router/types.ts
Normal file
79
packages/core/src/contexts/router/types.ts
Normal 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 }>
|
||||
>;
|
||||
};
|
||||
88
packages/core/src/contexts/undoableQueue/index.spec.tsx
Normal file
88
packages/core/src/contexts/undoableQueue/index.spec.tsx
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
90
packages/core/src/contexts/undoableQueue/index.tsx
Normal file
90
packages/core/src/contexts/undoableQueue/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
22
packages/core/src/contexts/undoableQueue/types.ts
Normal file
22
packages/core/src/contexts/undoableQueue/types.ts
Normal 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>;
|
||||
}
|
||||
17
packages/core/src/contexts/unsavedWarn/index.tsx
Normal file
17
packages/core/src/contexts/unsavedWarn/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
4
packages/core/src/contexts/unsavedWarn/types.ts
Normal file
4
packages/core/src/contexts/unsavedWarn/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface IUnsavedWarnContext {
|
||||
warnWhen?: boolean;
|
||||
setWarnWhen?: (value: boolean) => void;
|
||||
}
|
||||
Reference in New Issue
Block a user