UNPKG

adminjs

Version:
620 lines (619 loc) 19.1 kB
import { IconProps, VariantType } from '@adminjs/design-system'; import AdminJS from '../../adminjs.js'; import { CurrentAdmin } from '../../current-admin.interface.js'; import ViewHelpers from '../utils/view-helpers/view-helpers.js'; import BaseRecord from '../adapters/record/base-record.js'; import BaseResource from '../adapters/resource/base-resource.js'; import ActionDecorator from '../decorators/action/action-decorator.js'; import { LayoutElement, LayoutElementFunction } from '../utils/layout-element-parser/index.js'; import { RecordJSON } from '../../frontend/interfaces/index.js'; import { type NoticeMessage } from '../../frontend/interfaces/noticeMessage.interface.js'; export type ActionQueryParameters = { sortBy?: string; direction?: 'asc' | 'desc'; filters?: Record<string, unknown>; perPage?: number; page?: number; }; export type ActionType = 'resource' | 'record' | 'bulk'; /** * Execution context for an action. It is passed to the {@link Action#handler}, * {@link Action#before} and {@link Action#after} functions. * * @memberof Action * @alias ActionContext */ export type ActionContext = { /** * current instance of AdminJS. You may use it to fetch other Resources by their names: */ _admin: AdminJS; /** * Resource on which action has been invoked. Null for dashboard handler. */ resource: BaseResource; /** * Record on which action has been invoked (only for {@link actionType} === 'record') */ record?: BaseRecord; /** * Records on which action has been invoked (only for {@link actionType} === 'bulk') */ records?: Array<BaseRecord>; /** * view helpers */ h: ViewHelpers; /** * Object of currently invoked function. Not present for dashboard action */ action: ActionDecorator; /** * Currently logged in admin */ currentAdmin?: CurrentAdmin; /** * Any custom property which you can add to context */ [key: string]: any; }; /** * Context object passed to a PageHandler * * @alias PageContext * @memberof AdminJSOptions */ export type PageContext = { /** * current instance of AdminJS. You may use it to fetch other Resources by their names: */ _admin: AdminJS; /** * Currently logged in admin */ currentAdmin?: CurrentAdmin; /** * view helpers */ h: ViewHelpers; }; /** * ActionRequest * @memberof Action * @alias ActionRequest */ export type ActionRequest = { /** * parameters passed in an URL */ params: { /** * Id of current resource */ resourceId: string; /** * Id of current record (in case of record action) */ recordId?: string; /** * Id of selected records (in case of bulk action) divided by commas */ recordIds?: string; /** * Name of an action */ action: string; /** * an optional search query string (for `search` resource action) */ query?: string; [key: string]: any; }; /** * POST data passed to the backend */ payload?: Record<string, any>; /** * Elements of query string */ query?: Record<string, any>; /** * HTTP method */ method: 'post' | 'get'; }; /** * Base response for all actions * @memberof Action * @alias ActionResponse */ export type ActionResponse = { /** * Notice message which should be presented to the end user after showing the action */ notice?: NoticeMessage; /** * redirect path */ redirectUrl?: string; /** * Any other custom parameter */ [key: string]: any; }; /** * @description * Defines the type of {@link Action#isAccessible} and {@link Action#isVisible} functions * @alias IsFunction * @memberof Action */ export type IsFunction = (context: ActionContext) => boolean; /** * Required response of a Record action. Extends {@link ActionResponse} * * @memberof Action * @alias RecordActionResponse */ export type RecordActionResponse = ActionResponse & { /** * Record object. */ record: RecordJSON; }; /** * Required response of a Record action. Extends {@link ActionResponse} * * @memberof Action * @alias RecordActionResponse */ export type BulkActionResponse = ActionResponse & { /** * Array of RecordJSON objects. */ records: Array<RecordJSON>; }; /** * Type of a handler function. It has to return response compatible * with {@link ActionResponse}, {@link BulkActionResponse} or {@link RecordActionResponse} * * @alias ActionHandler * @async * @memberof Action * @returns {T | Promise<T>} */ export type ActionHandler<T> = (request: ActionRequest, response: any, context: ActionContext) => T | Promise<T>; /** * Before action hook. When it is given - it is performed before the {@link ActionHandler} * method. * @alias Before * @returns {ActionRequest | Promise<ActionRequest>} * @memberof Action * @async */ export type Before = ( /** * Request object */ request: ActionRequest, /** * Invocation context */ context: ActionContext) => ActionRequest | Promise<ActionRequest>; /** * Type of an after hook action. * * @memberof Action * @alias After * @async */ export type After<T> = ( /** * Response returned by the default ActionHandler */ response: T, /** * Original request which has been sent to ActionHandler */ request: ActionRequest, /** * Invocation context */ context: ActionContext) => T | Promise<T>; export type BuildInActions = 'show' | 'edit' | 'list' | 'delete' | 'bulkDelete' | 'new' | 'search'; /** * @classdesc * Interface representing an Action in AdminJS. * Look at {@tutorial actions} to see where you can use this interface. * * #### Example Action * * ``` * const action = { * actionType: 'record', * icon: 'View', * isVisible: true, * handler: async () => {...}, * component: 'MyAction', * } * ``` * * There are 3 kinds of actions: * * 1. Resource action, which is performed for an entire resource. * 2. Record action, invoked for an record in a resource * 3. Bulk action, invoked for an set of records in a resource * * ...and there are 7 actions predefined in AdminJS * * 1. {@link module:NewAction new} (resource action) - create new records in a resource * 2. {@link module:ListAction list} (resource action) - list all records within a resource * 3. {@link module:SearchAction search} (resource action) - search by query string * 4. {@link module:EditAction edit} (record action) - update records in a resource * 5. {@link module:ShowAction show} (record action) - show details of given record * 6. {@link module:DeleteAction delete} (record action) - delete given record * 7. {@link module:BulkDeleteAction bulkDelete} (bulk action) - delete given records * * Users can also create their own actions or override those already existing by using * {@link ResourceOptions} * * ```javascript * const AdminJSOptions = { * resources: [{ * resource: User, * options: { * actions: { * // example of overriding existing 'new' action for * // User resource. * new: { * icon: 'Add' * }, * // Example of creating a new 'myNewAction' which will be * // a resource action available for User model * myNewAction: { * actionType: 'resource', * handler: async (request, response, context) => {...} * } * } * } * }] * } * * const { ACTIONS } = require('adminjs') * // example of adding after filter for 'show' action for all resources * ACTIONS.show.after = async () => {...} * ``` */ export interface Action<T extends ActionResponse> { /** * Name of an action which is its uniq key. * If you use one of _list_, _search_, _edit_, _new_, _show_, _delete_ or * _bulkDelete_ you override existing actions. * For all other keys you create a new action. */ name: BuildInActions | string; /** * indicates if action should be visible for given invocation context. * It also can be a simple boolean value. * `True` by default. * The most common example of usage is to hide resources from the UI. * So let say we have 2 resources __User__ and __Cars__: * * ```javascript * const User = mongoose.model('User', mongoose.Schema({ * email: String, * encryptedPassword: String, * })) * const Car = mongoose.model('Car', mongoose.Schema({ * name: String, * ownerId: { type: mongoose.Types.ObjectId, ref: 'User' }, * }) * ``` * * so if we want to hide Users collection, but allow people to pick user when * creating cars. We can do this like this: * * ```javascript * new AdminJS({ resources: [{ * resource: User, * options: { actions: { list: { isVisible: false } } } * }]}) * ``` * In contrast - when we use {@link Action#isAccessible} instead - user wont be able to * pick car owner. * * @see {@link ActionContext} parameter passed to isAccessible * @see {@link IsFunction} exact type of the function */ isVisible?: boolean | IsFunction; /** * Indicates if the action can be invoked for given invocation context. * You can pass a boolean or function of type {@link IsFunction}, which * takes {@link ActionContext} as an argument. * * You can use it as a carrier between the hooks. * * Example for isVisible function which allows the user to edit cars which belongs only * to her: * * ```javascript * const canEditCars = ({ currentAdmin, record }) => { * return currentAdmin && ( * currentAdmin.role === 'admin' * || currentAdmin._id === record.param('ownerId') * ) * } * * new AdminJS({ resources: [{ * resource: Car, * options: { actions: { edit: { isAccessible: canEditCars } } } * }]}) * ``` * * @see {@link ActionContext} parameter passed to isAccessible * @see {@link IsFunction} exact type of the function */ isAccessible?: boolean | IsFunction; /** * If filter should be visible on the sidebar. Only for _resource_ actions * * Example of creating new resource action with filter * * ```javascript * new AdminJS({ resources: [{ * resource: Car, * options: { actions: { * newAction: { * type: 'resource', * showFilter: true, * } * }} * }]}) * ``` */ showFilter?: boolean; /** * If action should have resource actions buttons displayed above action header. * * Defaults to `true` * * @new in version v5.8.1 */ showResourceActions?: boolean; /** * Type of an action - could be either _resource_, _record_ or _bulk_. * * <img src="./images/actions.png"> * * When you define a new action - it is required. */ actionType: ActionType; /** * icon name for the action. Take a look {@link Icon} component, * because what you put here is passed down to it. * * ```javascript * new AdminJS({ resources: [{ * resource: Car, * options: { actions: { edit: { icon: 'Add' } } }, * }]}) * ``` */ icon?: IconProps['icon']; /** * guard message - user will have to confirm it before executing an action. * * ```javascript * new AdminJS({ resources: [{ * resource: Car, * options: { actions: { * delete: { * guard: 'doYouReallyWantToDoThis', * } * }} * }]}) * ``` * * What you enter there goes to a translate function, * so in order to define the actual message you will have to specify its * translation in {@link AdminJSOptions.Locale} */ guard?: string; /** * Component which will be used to render the action. To pass the component * use {@link ComponentLoader.add} or {@link ComponentLoader.override} method. * * Action components accepts {@link ActionProps} and are rendered by the * {@link BaseActionComponent} * * When component is set to `false` then action doesn't have it's own view. * Instead after clicking button it is immediately performed. Example of * an action without a view is {@link module:DeleteAction}. */ component?: string | false; /** * handler function which will be invoked by either: * - {@link ApiController#resourceAction} * - {@link ApiController#recordAction} * - or {@link ApiController#bulkAction} * when user visits clicks action link. * * If you are defining this action for a record it has to return: * - {@link ActionResponse} for resource action * - {@link RecordActionResponse} for record action * - {@link BulkActionResponse} for bulk action * * ```javascript * // Handler of a 'record' action * handler: async (request, response, context) { * const user = context.record * const Cars = context._admin.findResource('Car') * const userCar = Car.findOne(context.record.param('carId')) * return { * record: user.toJSON(context.currentAdmin), * } * } * ``` * * Required for new actions. For modifying already defined actions * like new and edit we suggest using {@link Action#before} and {@link Action#after} hooks. */ handler: ActionHandler<T> | Array<ActionHandler<T>> | null; /** * Before action hook. When it is given - it is performed before the {@link Action#handler} * method. * * Example of hashing password before creating it: * * ```javascript * actions: { * new: { * before: async (request) => { * if(request.payload.password) { * request.payload = { * ...request.payload, * encryptedPassword: await bcrypt.hash(request.payload.password, 10), * password: undefined, * } * } * return request * }, * } * } * ``` */ before?: Before | Array<Before>; /** * After action hook. When it is given - it is performed on the returned, * by {@link Action#handler handler} function response. * * You can use it to (just an idea) * - create log of changes done in the app * - prefetch additional data after original {@link Handler} is being performed * * Creating a changelog example: * * ```javascript * // example mongoose model * const ChangeLog = mongoose.model('ChangeLog', mongoose.Schema({ * // what action * action: { type: String }, * // who * userId: { type: mongoose.Types.ObjectId, ref: 'User' }, * // on which resource * resource: { type: String }, * // was record involved (resource and recordId creates to polymorphic relation) * recordId: { type: mongoose.Types.ObjectId }, * }, { timestamps: true })) * * // actual after function * const createLog = async (originalResponse, request, context) => { * // checking if object doesn't have any errors or is a delete action * if ((request.method === 'post' * && originalResponse.record * && !Object.keys(originalResponse.record.errors).length) * || context.action.name === 'delete') { * await ChangeLog.create({ * action: context.action.name, * // assuming in the session we store _id of the current admin * userId: context.currentAdmin && context.currentAdmin._id, * resource: context.resource.id(), * recordId: context.record && context.record.id(), * }) * } * return originalResponse * } * * // and attaching this function to actions for all resources * const { ACTIONS } = require('adminjs') * * ACTIONS.edit.after = [createLog] * ACTIONS.delete.after = [createLog] * ACTIONS.new.after = [createLog] * ``` * */ after?: After<T> | Array<After<T>>; /** * Indicates if given action should be seen in a drawer or in a full screen. Default to false */ showInDrawer?: boolean; /** * Indicates if Action Header should be hidden. * Action header consist of: * - breadcrumbs * - action buttons * - action title */ hideActionHeader?: boolean; /** * The max width of action HTML container. * You can put here an actual size in px or an array of widths, where different values * will be responsible for different breakpoints. * It is directly passed to action's wrapping {@link Box} component, to its `width` property. * * Examples * ```javascript * * // passing regular string * containerWidth: '800px' * * // passing number for 100% width * containerWidth: 1 * * // passing values for different {@link breakpoints} * containerWidth: [1, 1/2, 1/3] * ``` */ containerWidth?: string | number | Array<string | number>; /** * Definition for the layout. Works with the edit and show actions. * * With the help of {@link LayoutElement} you can put all the properties to whatever * layout you like, without knowing React. * * This is an example of defining a layout * * ``` * const layout = [{ width: 1 / 2 }, [ * ['@H3', { children: 'Company data' }], * 'companyName', * 'companySize', * ]], * [ * ['@H3', { children: 'Contact Info' }], * [{ flexDirection: 'row', flex: true }, [ * ['email', { pr: 'default', flexGrow: 1 }], * ['address', { flexGrow: 1 }], * ]], * ], * ] * ``` * * Alternatively you can pass a {@link LayoutElementFunction function} taking * {@link CurrentAdmin} as an argument. This will allow you to show/hide * given property for restricted users. * * To see entire documentation and more examples visit {@link LayoutElement} * * @see LayoutElement * @see LayoutElementFunction */ layout?: LayoutElementFunction | Array<LayoutElement>; /** * Defines the variant of the action. based on that it will receive given color. * @new in version v3.3 */ variant?: VariantType; /** * Action can be nested. If you give here another action name - it will be nested under it. * If parent action doesn't exists - it will be nested under name in the parent. * @new in version v3.3 */ parent?: string; /** * Any custom properties you want to pass down to {@link ActionJSON}. They have to * be stringified. * @new in version v3.3 */ custom?: Record<string, any>; }