import AdminBro from '../../admin-bro'
import { CurrentAdmin } from '../../current-admin.interface'
import ViewHelpers from '../utils/view-helpers'
import BaseRecord from '../adapters/base-record'
import BaseResource from '../adapters/base-resource'
import ActionDecorator from '../decorators/action-decorator'
import RecordJSON from '../decorators/record-json.interface'
import { NoticeMessage } from '../../frontend/store/with-notice'
/**
* 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 AdminBro. You may use it to fetch other Resources by their names:
*/
_admin: AdminBro;
/**
* 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;
}
/**
* Context object passed to a PageHandler
*
* @alias PageContext
* @memberof AdminBroOptions
*/
export type PageContext = {
/**
* current instance of AdminBro. You may use it to fetch other Resources by their names:
*/
_admin: AdminBro;
/**
* 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;
[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 {Promise<T>}
*/
export type ActionHandler<T> = (
request: ActionRequest,
response: any,
context: ActionContext
) => Promise<T>
/**
* Before action hook. When it is given - it is performed before the {@link ActionHandler}
* method.
* @alias Before
* @returns {Promise<ActionRequest>}
* @memberof Action
* @async
*/
export type Before = (
/**
* Request object
*/
request: ActionRequest,
/**
* Invocation context
*/
context: ActionContext,
) => 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,
) => Promise<T>
/**
* @classdesc
* Interface representing an Action in AdminBro.
* Look at {@tutorial 05-actions} to see where you can use this interface.
*
* #### Example Action
*
* ```
* const action = {
* actionType: 'record',
* label: 'Publish',
* icon: 'fas fa-eye',
* isVisible: true,
* handler: async () => {...},
* component: AdminBro.bundle('./my-action-component'),
* }
* ```
*
* 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
* 2. Bulk action, invoked for an set of records in a resource
*
* ...and there are 6 actions predefined in AdminBro
*
* 1. {@link module:NewAction new} (resource action) - create new records in a resource
* 1. {@link module:ListAction list} (resource action) - list all records within a resource
* 2. {@link module:EditAction edit} (record action) - update records in a resource
* 3. {@link module:ShowAction show} (record action) - show details of given record
* 3. {@link module:DeleteAction delete} (record action) - delete given record
* 3. {@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 AdminBroOptions = {
* resources: [{
* resource: User,
* options: {
* actions: {
* // example of overriding existing 'new' action for
* // User resource.
* new: {
* label: 'Create new record'
* },
* // 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('admin-bro')
* // example of adding after filter for 'show' action for all resources
* ACTIONS.show.after = async () => {...}
* ```
*/
export default interface Action <T extends ActionResponse> {
/**
* Name of an action which is its uniq key.
* If use one of _list_, _edit_, _new_, _show_ or _delete_ you override existing actions.
* For all other keys you create a new action.
*/
name: 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 AdminBro({ 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.
*
* Example for isVisible function which allows 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 AdminBro({ 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;
/**
* name of the action which will appear in the UI
*/
label?: string;
/**
* if filter should be visible on the sidebar. Only for _resource_ actions
*
* Example of creating new resource action with filter
*
* ```javascript
* new AdminBro({ resources: [{
* resource: Car,
* options: { actions: {
* newAction: {
* label: 'New action',
* type: 'resource',
* showFilter: true,
* }
* }}
* }]})
* ```
*/
showFilter?: 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: 'resource' | 'record' | 'bulk';
/**
* icon class of an action
*
* ```javascript
* new AdminBro({ resources: [{
* resource: Car,
* options: { actions: { edit: { icon: 'fa fa-bomb' } } },
* }]})
* ```
*/
icon?: string;
/**
* guard message - user will have to confirm it before executing an action.
*
* ```javascript
* new AdminBro({ resources: [{
* resource: Car,
* options: { actions: {
* delete: {
* guard: 'do you really want to delete this amazing element?',
* }
* }}
* }]})
* ```
*/
guard?: string;
/**
* Component which will be used to render the action. To pass the component
* use {@link AdminBro.bundle} 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 please use {@link Action#before} and {@link Action#after} hooks.
*/
handler: ActionHandler<T>;
/**
* 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.record.password) {
* request.payload.record = {
* ...request.payload.record,
* encryptedPassword: await bcrypt.hash(request.payload.record.password, 10),
* password: undefined,
* }
* }
* return request
* },
* }
* }
* ```
*/
before?: 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('admin-bro')
*
* ACTIONS.edit.after = createLog
* ACTIONS.delete.after = createLog
* ACTIONS.new.after = createLog
* ```
*
*/
after?: After<T>;
}
Source