Skip to content
Permalink
master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
* feat: replace AdminJS.bundle with ComponentLoader

* chore: remove unused admin-e2e directory

* feat: implement component loading in features

* fix: make ComponentLoader compatible with .bundle

* fix: move ComponentLoader to src/backend/utils

* feat: add detailed warning for missing components
5 contributors

Users who have contributed to this file

@wojtek-krysiak @dziraf @paweljaroszSB @leibowitz @RiledUpCrow
import { VariantType } from '@adminjs/design-system'
import AdminJS from '../../adminjs'
import { CurrentAdmin } from '../../current-admin.interface'
import ViewHelpers from '../utils/view-helpers/view-helpers'
import BaseRecord from '../adapters/record/base-record'
import BaseResource from '../adapters/resource/base-resource'
import ActionDecorator from '../decorators/action/action-decorator'
import { LayoutElement, LayoutElementFunction } from '../utils/layout-element-parser'
import { RecordJSON } from '../../frontend/interfaces'
import { NoticeMessage } from '../../frontend/hoc/with-notice'
import { TranslateFunctions } from '../../utils/translate-functions.factory'
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.
*
* Apart from the properties defined below it also extends {@link TranslateFunctions}.
* So you can use i.e. context.translateMessage(...) and others...
*
* @property {TranslateFunction} {...} all functions from {@link TranslateFunctions}
* interface.
*
* @memberof Action
* @alias ActionContext
*/
export type ActionContext = TranslateFunctions & {
/**
* 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 {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>
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?: string;
/**
* 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 {@link TranslateFunctions#translateMessage} 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>;
}