Skip to content

Instantly share code, notes, and snippets.

@wise-introvert
Created February 10, 2026 15:25
Show Gist options
  • Select an option

  • Save wise-introvert/1ed5388420eebdeb7ce861c322faf689 to your computer and use it in GitHub Desktop.

Select an option

Save wise-introvert/1ed5388420eebdeb7ce861c322faf689 to your computer and use it in GitHub Desktop.
Directory structure:
└── src/
├── babel.test.config.json
├── constants.ts
├── core-scripts.interface.ts
├── current-admin.interface.ts
├── index.ts
├── backend/
│ ├── index.ts
│ ├── actions/
│ │ └── index.ts
│ ├── adapters/
│ │ ├── index.ts
│ │ ├── database/
│ │ │ └── index.ts
│ │ ├── property/
│ │ │ └── index.ts
│ │ ├── record/
│ │ │ ├── index.ts
│ │ │ └── params.type.ts
│ │ └── resource/
│ │ ├── index.ts
│ │ └── supported-databases.type.ts
│ ├── bundler/
│ │ ├── index.ts
│ │ └── utils/
│ │ └── constants.ts
│ ├── controllers/
│ │ └── index.ts
│ ├── decorators/
│ │ ├── index.ts
│ │ ├── action/
│ │ │ └── index.ts
│ │ ├── property/
│ │ │ ├── index.ts
│ │ │ └── utils/
│ │ │ ├── index.ts
│ │ │ ├── override-from-options.spec.ts
│ │ │ └── override-from-options.ts
│ │ └── resource/
│ │ ├── index.ts
│ │ └── utils/
│ │ ├── flat-sub-properties.ts
│ │ └── index.ts
│ ├── services/
│ │ ├── index.ts
│ │ ├── action-error-handler/
│ │ │ └── index.ts
│ │ └── sort-setter/
│ │ └── index.ts
│ └── utils/
│ ├── index.ts
│ ├── uploaded-file.type.ts
│ ├── auth/
│ │ ├── default-auth-provider.ts
│ │ └── index.ts
│ ├── build-feature/
│ │ └── index.ts
│ ├── errors/
│ │ ├── configuration-error.ts
│ │ ├── forbidden-error.ts
│ │ ├── index.ts
│ │ ├── not-implemented-error.ts
│ │ └── record-error.ts
│ ├── filter/
│ │ └── index.ts
│ ├── layout-element-parser/
│ │ └── index.ts
│ ├── options-parser/
│ │ └── index.ts
│ ├── populator/
│ │ ├── index.ts
│ │ ├── populator.spec.ts
│ │ └── populator.ts
│ ├── request-parser/
│ │ └── index.ts
│ ├── resources-factory/
│ │ └── index.ts
│ ├── router/
│ │ ├── index.ts
│ │ └── router.spec.ts
│ └── view-helpers/
│ ├── index.ts
│ └── view-helpers.spec.ts
├── frontend/
│ ├── index.ts
│ ├── login-template.spec.ts
│ ├── components/
│ │ ├── index.ts
│ │ ├── actions/
│ │ │ ├── action.props.ts
│ │ │ ├── index.ts
│ │ │ └── utils/
│ │ │ └── index.ts
│ │ ├── app/
│ │ │ ├── admin-modal.tsx
│ │ │ ├── app-loader.tsx
│ │ │ ├── auth-background-component.tsx
│ │ │ ├── error-boundary.tsx
│ │ │ ├── footer.tsx
│ │ │ ├── index.ts
│ │ │ ├── action-button/
│ │ │ │ └── index.ts
│ │ │ ├── action-header/
│ │ │ │ ├── action-header-props.tsx
│ │ │ │ └── index.ts
│ │ │ ├── language-select/
│ │ │ │ └── index.ts
│ │ │ ├── records-table/
│ │ │ │ ├── index.ts
│ │ │ │ └── utils/
│ │ │ │ ├── display.tsx
│ │ │ │ └── get-bulk-actions-from-records.ts
│ │ │ └── sidebar/
│ │ │ ├── index.ts
│ │ │ └── sidebar-footer.tsx
│ │ ├── property-type/
│ │ │ ├── clean-property-component.tsx
│ │ │ ├── record-property-is-equal.ts
│ │ │ ├── array/
│ │ │ │ ├── add-new-item-translation.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── list.tsx
│ │ │ ├── boolean/
│ │ │ │ ├── boolean-property-value.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── list.tsx
│ │ │ │ ├── map-value.tsx
│ │ │ │ └── show.tsx
│ │ │ ├── currency/
│ │ │ │ ├── filter.tsx
│ │ │ │ ├── format-value.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── list.tsx
│ │ │ │ └── show.tsx
│ │ │ ├── datetime/
│ │ │ │ ├── index.ts
│ │ │ │ ├── list.tsx
│ │ │ │ ├── map-value.ts
│ │ │ │ ├── show.tsx
│ │ │ │ └── strip-time-from-iso.ts
│ │ │ ├── default-type/
│ │ │ │ ├── index.ts
│ │ │ │ ├── list.tsx
│ │ │ │ └── show.tsx
│ │ │ ├── docs/
│ │ │ │ └── on-property-change.doc.md
│ │ │ ├── key-value/
│ │ │ │ └── index.ts
│ │ │ ├── mixed/
│ │ │ │ ├── convert-to-sub-property.ts
│ │ │ │ └── index.ts
│ │ │ ├── password/
│ │ │ │ └── index.ts
│ │ │ ├── phone/
│ │ │ │ ├── filter.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── list.tsx
│ │ │ │ └── show.tsx
│ │ │ ├── reference/
│ │ │ │ ├── index.ts
│ │ │ │ ├── list.tsx
│ │ │ │ └── show.tsx
│ │ │ ├── richtext/
│ │ │ │ ├── index.ts
│ │ │ │ ├── list.tsx
│ │ │ │ └── show.tsx
│ │ │ ├── textarea/
│ │ │ │ ├── index.ts
│ │ │ │ └── show.tsx
│ │ │ └── utils/
│ │ │ ├── index.ts
│ │ │ ├── property-description/
│ │ │ │ └── index.ts
│ │ │ └── property-label/
│ │ │ └── index.ts
│ │ ├── routes/
│ │ │ ├── index.ts
│ │ │ └── utils/
│ │ │ └── should-action-re-fetch-data.ts
│ │ └── spec/
│ │ ├── action-json.factory.ts
│ │ ├── factory.ts
│ │ ├── initialize-translations.ts
│ │ ├── page-json.factory.ts
│ │ ├── property-json.factory.ts
│ │ └── test-context-provider.tsx
│ ├── hoc/
│ │ ├── index.ts
│ │ └── with-no-ssr.tsx
│ ├── hooks/
│ │ ├── index.ts
│ │ ├── use-filter-drawer.tsx
│ │ ├── use-notice.ts
│ │ ├── use-action/
│ │ │ ├── index.ts
│ │ │ ├── use-action-response-handler.ts
│ │ │ └── use-action.doc.md
│ │ ├── use-local-storage/
│ │ │ ├── index.ts
│ │ │ ├── use-local-storage-result.type.ts
│ │ │ └── use-local-storage.doc.md
│ │ ├── use-record/
│ │ │ ├── filter-record.ts
│ │ │ ├── index.ts
│ │ │ ├── is-entire-record-given.ts
│ │ │ └── merge-record-response.ts
│ │ ├── use-records/
│ │ │ ├── index.ts
│ │ │ └── use-records-result.type.ts
│ │ ├── use-resource/
│ │ │ ├── index.ts
│ │ │ ├── use-resource.doc.md
│ │ │ └── use-resource.ts
│ │ └── use-selected-records/
│ │ ├── index.ts
│ │ └── use-selected-records-result.type.ts
│ ├── interfaces/
│ │ ├── index.ts
│ │ ├── modal.interface.ts
│ │ ├── noticeMessage.interface.ts
│ │ ├── page-json.interface.ts
│ │ ├── action/
│ │ │ ├── action-has-component.ts
│ │ │ ├── build-action-test-id.ts
│ │ │ ├── index.ts
│ │ │ ├── is-bulk-action.ts
│ │ │ ├── is-record-action.ts
│ │ │ └── is-resource-action.ts
│ │ └── property-json/
│ │ └── index.ts
│ ├── store/
│ │ ├── index.ts
│ │ ├── actions/
│ │ │ ├── add-notice.ts
│ │ │ ├── drop-notice.ts
│ │ │ ├── filter-drawer.ts
│ │ │ ├── index.ts
│ │ │ ├── initialize-assets.ts
│ │ │ ├── initialize-branding.ts
│ │ │ ├── initialize-dashboard.ts
│ │ │ ├── initialize-locale.ts
│ │ │ ├── initialize-pages.ts
│ │ │ ├── initialize-paths.ts
│ │ │ ├── initialize-resources.ts
│ │ │ ├── initialize-theme.ts
│ │ │ ├── initialize-versions.ts
│ │ │ ├── modal.ts
│ │ │ ├── route-changed.ts
│ │ │ ├── set-current-admin.ts
│ │ │ ├── set-drawer-preroute.ts
│ │ │ └── set-notice-progress.ts
│ │ ├── reducers/
│ │ │ ├── assetsReducer.ts
│ │ │ ├── brandingReducer.ts
│ │ │ ├── dashboardReducer.ts
│ │ │ ├── drawerReducer.ts
│ │ │ ├── filterDrawerReducer.ts
│ │ │ ├── index.ts
│ │ │ ├── localesReducer.ts
│ │ │ ├── modalReducer.ts
│ │ │ ├── pagesReducer.ts
│ │ │ ├── pathsReducer.ts
│ │ │ ├── resourcesReducer.ts
│ │ │ ├── routerReducer.ts
│ │ │ ├── sessionReducer.ts
│ │ │ ├── themeReducer.ts
│ │ │ └── versionsReducer.ts
│ │ └── utils/
│ │ └── pages-to-store.ts
│ └── utils/
│ ├── data-css-name.ts
│ └── index.ts
├── locale/
│ ├── default-config.ts
│ └── index.ts
└── utils/
├── error-type.enum.ts
├── index.ts
├── theme-bundler.ts
├── flat/
│ ├── constants.ts
│ ├── filter-out-params.doc.md
│ ├── filter-out-params.ts
│ ├── flat.types.ts
│ ├── get.doc.md
│ ├── index.ts
│ ├── merge.ts
│ ├── path-parts.type.ts
│ ├── path-to-parts.doc.md
│ ├── path-to-parts.ts
│ ├── property-key-regex.ts
│ ├── select-params.doc.md
│ └── set.doc.md
└── param-converter/
├── constants.ts
├── convert-nested-param.ts
├── convert-param.spec.ts
├── convert-param.ts
├── index.ts
└── param-converter-module.ts
Files Content:
================================================
FILE: src/babel.test.config.json
================================================
{
"presets": [
"@babel/preset-react",
[
"@babel/preset-env",
{
"targets": {
"node": "18"
},
"loose": true,
"modules": false
}
],
"@babel/preset-typescript"
],
"plugins": ["@babel/plugin-syntax-import-assertions"],
"only": ["src/", "spec/"],
"ignore": [
"src/frontend/assets/scripts/app-bundle.development.js",
"src/frontend/assets/scripts/app-bundle.production.js",
"src/frontend/assets/scripts/global-bundle.development.js",
"src/frontend/assets/scripts/global-bundle.production.js"
]
}
================================================
FILE: src/constants.ts
================================================
/* cspell: disable */
export const DOCS = 'https://docs.adminjs.co'
export const DEFAULT_PATHS = {
rootPath: '/admin',
logoutPath: '/admin/logout',
loginPath: '/admin/login',
refreshTokenPath: '/admin/refresh-token',
}
================================================
FILE: src/core-scripts.interface.ts
================================================
/**
* @memberof Assets
* @alias CoreScripts
*
* Optional mapping of core AdminJS browser scripts:
* - app.bundle.js
* - components.bundle.js
* - design-system.bundle.js
* - global.bundle.js
*
* You may want to use it if you'd like to version assets for caching. This
* will only work if you have also configured `assetsCDN` in AdminJS options.
*
* Example:
* ```
* {
* 'app.bundle.js': 'app.bundle.123456.js',
* 'components.bundle.js': 'components.bundle.123456.js',
* 'design-system.bundle.js': 'design-system.bundle.123456.js',
* 'global.bundle.js': 'global.bundle.123456.js',
* }
* ```
*/
export interface CoreScripts {
/**
* App Bundle
*/
'app.bundle.js': string;
/**
* Custom Components
*/
'components.bundle.js': string;
/**
* Design System Bundle
*/
'design-system.bundle.js': string;
/**
* Global bundle
*/
'global.bundle.js': string;
}
================================================
FILE: src/current-admin.interface.ts
================================================
/**
* Currently logged in admin.
*
* ### Usage with TypeScript
*
* ```typescript
* import { CurrentAdmin } from 'adminjs'
* ```
*
* @alias CurrentAdmin
* @memberof AdminJS
*/
export interface CurrentAdmin {
/**
* Admin has one required field which is an email
*/
email: string;
/**
* Optional title/role of an admin - this will be presented below the email
*/
title?: string;
/**
* Optional url for an avatar photo
*/
avatarUrl?: string;
/**
* Id of your admin user
*/
id?: string;
/**
* Optional ID of theme to use
*/
theme?: string;
/**
* Extra metadata specific to given Auth Provider
*/
_auth?: Record<string, any>;
/**
* Also you can put as many other fields to it as you like.
*/
[key: string]: any;
}
================================================
FILE: src/index.ts
================================================
import AdminJS from './adminjs.js'
export * from './backend/index.js'
export * from './frontend/index.js'
export * from './locale/index.js'
export * from './utils/index.js'
export * from './constants.js'
export * from './adminjs-options.interface.js'
export * from './current-admin.interface.js'
export default AdminJS
================================================
FILE: src/backend/index.ts
================================================
export * from './actions/index.js'
export * from './adapters/index.js'
export * from './controllers/index.js'
export * from './decorators/index.js'
export * from './services/index.js'
export * from './utils/index.js'
================================================
FILE: src/backend/actions/index.ts
================================================
import { DeleteAction } from './delete/delete-action.js'
import { ShowAction } from './show/show-action.js'
import { NewAction } from './new/new-action.js'
import { EditAction } from './edit/edit-action.js'
import { SearchAction } from './search/search-action.js'
import { ListAction } from './list/list-action.js'
import { BulkDeleteAction } from './bulk-delete/bulk-delete-action.js'
import { BuildInActions } from './action.interface.js'
export * from './delete/delete-action.js'
export * from './show/show-action.js'
export * from './new/new-action.js'
export * from './edit/edit-action.js'
export * from './search/search-action.js'
export * from './list/list-action.js'
export * from './bulk-delete/bulk-delete-action.js'
export * from './action.interface.js'
export const ACTIONS: {[key in BuildInActions]: any} = {
new: NewAction,
list: ListAction,
show: ShowAction,
edit: EditAction,
delete: DeleteAction,
bulkDelete: BulkDeleteAction,
search: SearchAction,
}
================================================
FILE: src/backend/adapters/index.ts
================================================
export * from './database/index.js'
export * from './property/index.js'
export * from './record/index.js'
export * from './resource/index.js'
================================================
FILE: src/backend/adapters/database/index.ts
================================================
export { default as BaseDatabase } from './base-database.js'
================================================
FILE: src/backend/adapters/property/index.ts
================================================
export { default as BaseProperty } from './base-property.js'
export type { PropertyType } from './base-property.js'
================================================
FILE: src/backend/adapters/record/index.ts
================================================
export { default as BaseRecord } from './base-record.js'
export * from './params.type.js'
================================================
FILE: src/backend/adapters/record/params.type.ts
================================================
/**
* @alias ParamsTypeValue
* @memberof BaseRecord
*/
export type ParamsTypeValue = string
| number
| boolean
| null
| undefined
| []
| Record<string, unknown>
| File
/**
* @alias ParamsType
* @memberof BaseRecord
*/
export type ParamsType = Record<string, any>
// TODO: change ^^^any to ParamsTypeValue
================================================
FILE: src/backend/adapters/resource/index.ts
================================================
export { default as BaseResource } from './base-resource.js'
export * from './supported-databases.type.js'
================================================
FILE: src/backend/adapters/resource/supported-databases.type.ts
================================================
export type SupportedDatabasesType = 'MySQL'
| 'MariaDB'
| 'Postgres'
| 'CockroachDB'
| 'SQLite'
| 'MicrosoftSQLServer'
| 'Oracle'
| 'SAPHana'
| 'MongoDB'
| 'other'
================================================
FILE: src/backend/bundler/index.ts
================================================
export { default as appBundler } from './app.bundler.js'
export { default as globalsBundler } from './globals.bundler.js'
export { default as componentsBundler } from './components.bundler.js'
export { default as generateUserComponentEntry } from './generate-user-component-entry.js'
export * from './utils/constants.js'
export { AssetBundler } from './utils/asset-bundler.js'
================================================
FILE: src/backend/bundler/utils/constants.ts
================================================
import path from 'path'
export const NODE_ENV = process.env.NODE_ENV === 'production' ? 'production' : 'development'
const DEFAULT_TMP_DIR = '.adminjs'
export const ADMIN_JS_TMP_DIR = typeof process === 'object'
? process.env.ADMIN_JS_TMP_DIR || DEFAULT_TMP_DIR
: DEFAULT_TMP_DIR
export const COMPONENTS_ENTRY_PATH = path.join(ADMIN_JS_TMP_DIR, 'entry.js')
export const COMPONENTS_OUTPUT_PATH = path.join(ADMIN_JS_TMP_DIR, 'bundle.js')
================================================
FILE: src/backend/controllers/index.ts
================================================
export { default as AppController } from './app-controller.js'
export { default as ApiController } from './api-controller.js'
================================================
FILE: src/backend/decorators/index.ts
================================================
export * from './action/index.js'
export * from './property/index.js'
export * from './resource/index.js'
================================================
FILE: src/backend/decorators/action/index.ts
================================================
export { default as ActionDecorator } from './action-decorator.js'
================================================
FILE: src/backend/decorators/property/index.ts
================================================
export { PropertyDecorator } from './property-decorator.js'
export type { default as PropertyOptions } from './property-options.interface.js'
export * from './property-options.interface.js'
================================================
FILE: src/backend/decorators/property/utils/index.ts
================================================
export * from './override-from-options.js'
================================================
FILE: src/backend/decorators/property/utils/override-from-options.spec.ts
================================================
import { expect } from 'chai'
import sinon from 'sinon'
import { PropertyType, BaseProperty } from '../../../adapters/property/index.js'
import { overrideFromOptions } from './override-from-options.js'
describe('overrideFromOptions', () => {
const propertyName = 'type'
const rawValue = 'boolean'
const optionsValue = 'string'
let property: BaseProperty
beforeEach(() => {
property = sinon.createStubInstance(BaseProperty, {
[propertyName]: sinon.stub<[], PropertyType>().returns(rawValue),
}) as unknown as BaseProperty
})
it('returns value from BaseProperty function when options are not given', () => {
expect(overrideFromOptions(propertyName, property, {})).to.eq(rawValue)
})
it('returns value from options it is given', () => {
expect(overrideFromOptions(propertyName, property, {
[propertyName]: optionsValue,
})).to.eq(optionsValue)
})
})
================================================
FILE: src/backend/decorators/property/utils/override-from-options.ts
================================================
import { BaseProperty } from '../../../adapters/property/index.js'
import PropertyOptions from '../property-options.interface.js'
export type OverridableFromOptionsType = keyof Pick<BaseProperty,
'isSortable' |
'type' |
'isId' |
'isRequired' |
'isTitle'
>
export function overrideFromOptions<T extends OverridableFromOptionsType>(
optionName: T,
property: BaseProperty,
options: PropertyOptions,
): ReturnType<BaseProperty[OverridableFromOptionsType]> | null | undefined {
if (typeof options[optionName] === 'undefined') {
return property[optionName]()
}
return options[optionName]
}
================================================
FILE: src/backend/decorators/resource/index.ts
================================================
export { default as ResourceDecorator } from './resource-decorator.js'
export * from './resource-options.interface.js'
================================================
FILE: src/backend/decorators/resource/utils/flat-sub-properties.ts
================================================
import PropertyDecorator from '../../property/property-decorator.js'
/**
* Bu default all subProperties are nested as an array in root Property. This is easy for
* adapter to maintain. But in AdminJS core we need a fast way to access them by path.
*
* This function changes an array to object recursively (for nested subProperties) so they
* could be accessed via properties['path.to.sub.property']
*
* @param {PropertyDecorator} rootProperty
*
* @return {Record<PropertyDecorator>}
* @private
*/
export const flatSubProperties = (
rootProperty: PropertyDecorator,
): Record<string, PropertyDecorator> => (
rootProperty.subProperties().reduce((subMemo, subProperty) => Object.assign(
subMemo,
{ [subProperty.propertyPath]: subProperty },
flatSubProperties(subProperty),
), {})
)
================================================
FILE: src/backend/decorators/resource/utils/index.ts
================================================
export * from './find-sub-property.js'
export * from './flat-sub-properties.js'
export * from './get-navigation.js'
export * from './decorate-properties.js'
export * from './decorate-actions.js'
export * from './get-property-by-key.js'
================================================
FILE: src/backend/services/index.ts
================================================
export * from './action-error-handler/index.js'
export * from './sort-setter/index.js'
================================================
FILE: src/backend/services/action-error-handler/index.ts
================================================
export { default as actionErrorHandler } from './action-error-handler.js'
================================================
FILE: src/backend/services/sort-setter/index.ts
================================================
export { default as SortSetter } from './sort-setter.js'
================================================
FILE: src/backend/utils/index.ts
================================================
export * from './auth/index.js'
export * from './build-feature/index.js'
export * from './errors/index.js'
export * from './filter/index.js'
export * from './layout-element-parser/index.js'
export * from './options-parser/index.js'
export * from './populator/index.js'
export * from './request-parser/index.js'
export * from './resources-factory/index.js'
export * from './view-helpers/index.js'
export * from './router/index.js'
export * from './uploaded-file.type.js'
export * from './component-loader.js'
================================================
FILE: src/backend/utils/uploaded-file.type.ts
================================================
/**
* File uploaded via FormData to the backend.
*
* @memberof AdminJS
* @alias UploadedFile
*/
export type UploadedFile = {
/**
* The size of the uploaded file in bytes.
* this property says how many bytes of the file have been written to disk yet.
*/
size: number;
/**
* The path this file is being written to.
*/
path: string;
/**
* The mime type of this file, according to the uploading client.
*/
type: string;
/**
* The name this file had according to the uploading client.
*/
name: string | null;
}
================================================
FILE: src/backend/utils/auth/default-auth-provider.ts
================================================
import { AuthProviderConfig, AuthenticatePayload, BaseAuthProvider, LoginHandlerOptions } from './base-auth-provider.js'
export interface DefaultAuthenticatePayload extends AuthenticatePayload {
email: string;
password: string;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DefaultAuthProviderConfig extends AuthProviderConfig<DefaultAuthenticatePayload> {}
export class DefaultAuthProvider extends BaseAuthProvider {
protected readonly authenticate
constructor({ authenticate }: DefaultAuthProviderConfig) {
super()
this.authenticate = authenticate
}
override async handleLogin(opts: LoginHandlerOptions, context) {
const { data = {} } = opts
const { email, password } = data
return this.authenticate({ email, password }, context)
}
}
================================================
FILE: src/backend/utils/auth/index.ts
================================================
export * from './base-auth-provider.js'
export * from './default-auth-provider.js'
================================================
FILE: src/backend/utils/build-feature/index.ts
================================================
export * from './build-feature.js'
================================================
FILE: src/backend/utils/errors/configuration-error.ts
================================================
import { ErrorTypeEnum } from '../../../utils/error-type.enum.js'
import * as CONSTANTS from '../../../constants.js'
const buildUrl = (page: string): string => (
`${CONSTANTS.DOCS}/${page}`
)
/**
* Error which is thrown when user messed up something in the configuration
*
* @category Errors
*/
export class ConfigurationError extends Error {
/**
* @param {string} fnName name of the function, base on which error will
* print on the output link to the method documentation.
* @param {string} message
*/
constructor(message, fnName) {
const msg = `
${message}
More information can be found at: ${buildUrl(fnName)}
`
super(msg)
this.message = msg
this.name = ErrorTypeEnum.Configuration
}
}
export default ConfigurationError
================================================
FILE: src/backend/utils/errors/forbidden-error.ts
================================================
import { ErrorTypeEnum } from '../../../utils/error-type.enum.js'
import RecordError from './record-error.js'
/**
* Error which is thrown when user
* doesn't have an access to a given resource/action.
*
* @category Errors
*/
export class ForbiddenError extends Error {
/**
* HTTP Status code: 403
*/
public statusCode: number
/**
* Base error message and type which is stored in the record
*/
public baseError: RecordError
/**
* Any custom message which should be seen in the UI
*/
public baseMessage?: string
/**
* @param {string} [message]
*/
constructor(message?: string) {
const defaultMessage = 'You cannot perform this action'
super(defaultMessage)
this.statusCode = 403
this.baseMessage = message
this.baseError = {
message: message ?? defaultMessage,
type: ErrorTypeEnum.Forbidden,
}
this.name = ErrorTypeEnum.Forbidden
}
}
export default ForbiddenError
================================================
FILE: src/backend/utils/errors/index.ts
================================================
export * from './app-error.js'
export * from './configuration-error.js'
export * from './forbidden-error.js'
export * from './not-found-error.js'
export * from './not-implemented-error.js'
export * from './record-error.js'
export * from './validation-error.js'
================================================
FILE: src/backend/utils/errors/not-implemented-error.ts
================================================
import { DOCS } from '../../../constants.js'
const buildUrl = (fnName: string): string => {
if (fnName) {
let obj
let fn
if (fnName.indexOf('.') > 0) {
[obj, fn] = fnName.split('.')
fn = `.${fn}`
} else {
[obj, fn] = fnName.split('#')
}
return `${DOCS}/${obj}.html#${fn}`
}
return DOCS
}
/**
* Error which is thrown when an abstract method is not implemented
*
* @category Errors
*/
export class NotImplementedError extends Error {
/**
* @param {string} fnName name of the function, base on which error will
* print on the output link to the method documentation.
*/
constructor(fnName: string) {
const message = `
You have to implement the method: ${fnName}
Check out the documentation at: ${buildUrl(fnName)}
`
super(message)
this.message = message
}
}
export default NotImplementedError
================================================
FILE: src/backend/utils/errors/record-error.ts
================================================
import { ErrorTypeEnum } from '../../../utils/error-type.enum.js'
/**
* Record Error
* @alias RecordError
* @memberof ValidationError
*/
export type RecordError = {
/**
* error type (i.e. required)
*/
type?: ErrorTypeEnum | string
/**
* Code of message
*/
message: string
}
export default RecordError
================================================
FILE: src/backend/utils/filter/index.ts
================================================
export * from './filter.js'
================================================
FILE: src/backend/utils/layout-element-parser/index.ts
================================================
export * from './layout-element-parser.js'
================================================
FILE: src/backend/utils/options-parser/index.ts
================================================
export * from './options-parser.js'
================================================
FILE: src/backend/utils/populator/index.ts
================================================
export * from './populator.js'
export * from './populate-property.js'
================================================
FILE: src/backend/utils/populator/populator.spec.ts
================================================
import { expect } from 'chai'
import populator from './populator.js'
describe('populator', () => {
context('empty array given as params', () => {
it('returns empty array when no records are given', async () => {
const records = await populator([])
expect(records).to.have.lengthOf(0)
})
})
})
================================================
FILE: src/backend/utils/populator/populator.ts
================================================
import BaseRecord from '../../adapters/record/base-record.js'
import { populateProperty } from './populate-property.js'
import { ActionContext } from '../../actions/index.js'
/**
* @load ./populator.doc.md
* @param {Array<BaseRecord>} records
* @param context
* @new In version 3.3
*/
export async function populator(
records: Array<BaseRecord>,
context?:ActionContext,
): Promise<Array<BaseRecord>> {
if (!records || !records.length) {
return records
}
const resourceDecorator = records[0].resource.decorate()
const allProperties = Object.values(resourceDecorator.getFlattenProperties())
const references = allProperties.filter((p) => !!p.reference())
await Promise.all(references.map(async (propertyDecorator) => {
await populateProperty(records, propertyDecorator, context)
}))
return records
}
export default populator
================================================
FILE: src/backend/utils/request-parser/index.ts
================================================
export * from './request-parser.js'
================================================
FILE: src/backend/utils/resources-factory/index.ts
================================================
export { default as ResourcesFactory } from './resources-factory.js'
================================================
FILE: src/backend/utils/router/index.ts
================================================
export * from './router.js'
================================================
FILE: src/backend/utils/router/router.spec.ts
================================================
import { expect } from 'chai'
import Router from './router.js'
describe('Router', function () {
it('has both assets and routes', function () {
expect(Router.assets).not.to.be.undefined
expect(Router.routes).not.to.be.undefined
})
it('returns development bundle by default', function () {
const asset = Router.assets.find((a) => a.path === '/frontend/assets/app.bundle.js')
expect(asset && asset.src).to.contain('scripts/app-bundle.development.js')
})
})
================================================
FILE: src/backend/utils/view-helpers/index.ts
================================================
export * from './view-helpers.js'
================================================
FILE: src/backend/utils/view-helpers/view-helpers.spec.ts
================================================
import { expect } from 'chai'
import ViewHelpers from './view-helpers.js'
describe('ViewHelpers', function () {
describe('#urlBuilder', function () {
it('returns joined path for default rootUrl', function () {
const h = new ViewHelpers({})
expect(h.urlBuilder(['my', 'path'])).to.equal('/admin/my/path')
})
it('returns correct url when user gives admin root path not starting with /', function () {
const h = new ViewHelpers({ options: { rootPath: 'admin' } })
expect(h.urlBuilder(['my', 'path'])).to.equal('/admin/my/path')
})
it('returns correct url for rootPath set to /', function () {
const h = new ViewHelpers({ options: { rootPath: '/' } })
expect(h.urlBuilder(['my', 'path'])).to.equal('/my/path')
})
})
})
================================================
FILE: src/frontend/index.ts
================================================
export * from './components/index.js'
export * from './hoc/index.js'
export * from './hooks/index.js'
export * from './interfaces/index.js'
export * from './store/index.js'
export * from './utils/index.js'
================================================
FILE: src/frontend/login-template.spec.ts
================================================
import { expect } from 'chai'
import loginTemplate from './login-template.js'
import AdminJS from '../adminjs.js'
describe('login-template', function () {
const action = '/login'
it('renders error message', async function () {
const adminJs = new AdminJS({})
const errorMessage = 'Something went wrong'
const html = await loginTemplate(adminJs, { action, errorMessage })
expect(html).to.contain(errorMessage)
})
})
================================================
FILE: src/frontend/components/index.ts
================================================
export * from './actions/index.js'
export * from './app/index.js'
export * from './login/index.js'
export * from './property-type/index.js'
export * from './routes/index.js'
export * from './application.js'
================================================
FILE: src/frontend/components/actions/action.props.ts
================================================
import { Dispatch, SetStateAction } from 'react'
import { ActionJSON, RecordJSON, ResourceJSON } from '../../interfaces/index.js'
/**
* Props which are passed to all action components
* @alias ActionProps
* @memberof BaseActionComponent
*/
export type ActionProps = {
/**
* Action object describing the action
*/
action: ActionJSON;
/**
* Object of type: {@link ResourceJSON}
*/
resource: ResourceJSON;
/**
* Selected record. Passed for actions with "record" actionType
*/
record?: RecordJSON;
/**
* Selected records. Passed for actions with "bulk" actionType
*/
records?: Array<RecordJSON>;
/**
* Sets tag in a header of an action. It is a function taking tag as an argument
*/
setTag?: Dispatch<SetStateAction<string>>;
}
================================================
FILE: src/frontend/components/actions/index.ts
================================================
import { New } from './new.js'
import { Edit } from './edit.js'
import { Show } from './show.js'
import { List } from './list.js'
import { BulkDelete } from './bulk-delete.js'
export * from './new.js'
export * from './action.props.js'
export * from './edit.js'
export * from './show.js'
export * from './list.js'
export * from './bulk-delete.js'
export * from './utils/index.js'
export const actions = {
new: New,
edit: Edit,
show: Show,
list: List,
bulkDelete: BulkDelete,
}
================================================
FILE: src/frontend/components/actions/utils/index.ts
================================================
export * from './layout-element-renderer.js'
================================================
FILE: src/frontend/components/app/admin-modal.tsx
================================================
import { Modal } from '@adminjs/design-system'
import React, { FC } from 'react'
import { useSelector } from 'react-redux'
import { ReduxState } from '../../store/index.js'
export const AdminModal: FC = () => {
const modalState = useSelector((state: ReduxState) => state.modal)
return modalState.show ? <Modal {...modalState.modalProps} /> : null
}
export default AdminModal
================================================
FILE: src/frontend/components/app/app-loader.tsx
================================================
import { Box, Loader } from '@adminjs/design-system'
import React, { FC } from 'react'
export const AppLoader: FC = () => (
<Box width="100%" height="100%" flex alignItems="center" justifyContent="center">
<Loader />
</Box>
)
================================================
FILE: src/frontend/components/app/auth-background-component.tsx
================================================
import React from 'react'
import allowOverride from '../../hoc/allow-override.js'
const AuthenticationBackgroundComponent: React.FC = () => null
const OverridableAuthenticationBackgroundComponent = allowOverride(AuthenticationBackgroundComponent, 'AuthenticationBackgroundComponent')
export {
OverridableAuthenticationBackgroundComponent as default,
OverridableAuthenticationBackgroundComponent as AuthenticationBackgroundComponent,
AuthenticationBackgroundComponent as OriginalAuthenticationBackgroundComponent,
}
================================================
FILE: src/frontend/components/app/error-boundary.tsx
================================================
import React, { ReactNode } from 'react'
import { Text, MessageBox } from '@adminjs/design-system'
import { useTranslation } from '../../hooks/index.js'
type State = {
error: any;
}
const ErrorMessage: React.FC<State> = ({ error }) => {
const { translateMessage } = useTranslation()
return (
<MessageBox m="xxl" variant="danger" message="Javascript Error">
<Text>{error.toString()}</Text>
<Text mt="default">{translateMessage('seeConsoleForMore')}</Text>
</MessageBox>
)
}
export class ErrorBoundary extends React.Component<any, State> {
constructor(props) {
super(props)
this.state = {
error: null,
}
}
componentDidCatch(error): void {
this.setState({ error })
}
render(): ReactNode {
const { children } = this.props
const { error } = this.state
if (error !== null) {
return (<ErrorMessage error={error} />)
}
return children || null
}
}
export default ErrorBoundary
================================================
FILE: src/frontend/components/app/footer.tsx
================================================
import React from 'react'
import allowOverride from '../../hoc/allow-override.js'
const Footer: React.FC = () => null
const OverridableFooter = allowOverride(Footer, 'Footer')
export {
OverridableFooter as default,
OverridableFooter as Footer,
Footer as OriginalFooter,
}
================================================
FILE: src/frontend/components/app/index.ts
================================================
export * from './action-button/index.js'
export * from './action-header/index.js'
export * from './admin-modal.js'
export * from './app-loader.js'
export * from './auth-background-component.js'
export * from './base-action-component.js'
export * from './breadcrumbs.js'
export * from './default-dashboard.js'
export * from './drawer-portal.js'
export * from './error-boundary.js'
export * from './error-message.js'
export * from './filter-drawer.js'
export * from './logged-in.js'
export * from './notice.js'
export * from './records-table/index.js'
export * from './sidebar/index.js'
export * from './sort-link.js'
export { default as SortLink } from './sort-link.js'
export * from './top-bar.js'
export * from './version.js'
export * from './footer.js'
================================================
FILE: src/frontend/components/app/action-button/index.ts
================================================
export * from './action-button.js'
================================================
FILE: src/frontend/components/app/action-header/action-header-props.tsx
================================================
import { ActionJSON, RecordJSON, ResourceJSON } from '../../../interfaces/index.js'
import { ActionResponse } from '../../../../backend/actions/action.interface.js'
/**
* @memberof ActionHeader
* @alias ActionHeaderProps
*/
export type ActionHeaderProps = {
/** Resource for the action */
resource: ResourceJSON;
/** Optional record - for _record_ actions */
record?: RecordJSON;
/** If given, action header will render Filter button */
toggleFilter?: (() => any) | boolean;
/**
* It indicates if action without a component was performed.
*/
actionPerformed?: (action: ActionResponse) => any;
/** An action objet */
action: ActionJSON;
/** Optional tag which will be rendered as a {@link Badge} */
tag?: string;
/** If set, component wont render actions */
omitActions?: boolean;
};
================================================
FILE: src/frontend/components/app/action-header/index.ts
================================================
export * from './action-header.js'
export * from './action-header-props.js'
================================================
FILE: src/frontend/components/app/language-select/index.ts
================================================
export * from './language-select.js'
================================================
FILE: src/frontend/components/app/records-table/index.ts
================================================
export * from './no-records.js'
export * from './property-header.js'
export * from './record-in-list.js'
export * from './records-table-header.js'
export * from './records-table.js'
export * from './selected-records.js'
================================================
FILE: src/frontend/components/app/records-table/utils/display.tsx
================================================
export const display = (isTitle: boolean): Array<string> => [
isTitle ? 'table-cell' : 'none',
isTitle ? 'table-cell' : 'none',
'table-cell',
'table-cell',
]
================================================
FILE: src/frontend/components/app/records-table/utils/get-bulk-actions-from-records.ts
================================================
import { ActionJSON, RecordJSON } from '../../../../interfaces/index.js'
const getBulkActionsFromRecords = (records: Array<RecordJSON>): Array<ActionJSON> => {
const actions = Object.values(records.reduce((memo, record) => ({
...memo,
...record.bulkActions.reduce((actionsMemo, action) => ({
...actionsMemo,
[action.name]: action,
}), {} as Record<string, ActionJSON>),
}), {} as Record<string, ActionJSON>))
return actions
}
export default getBulkActionsFromRecords
================================================
FILE: src/frontend/components/app/sidebar/index.ts
================================================
import Sidebar from './sidebar.js'
export * from './sidebar-resource-section.js'
export { Sidebar }
================================================
FILE: src/frontend/components/app/sidebar/sidebar-footer.tsx
================================================
import React from 'react'
import { Box, MadeWithLove } from '@adminjs/design-system'
import { useSelector } from 'react-redux'
import { BrandingOptions } from '../../../../adminjs-options.interface.js'
import allowOverride from '../../../hoc/allow-override.js'
import { ReduxState } from '../../../store/index.js'
const SidebarFooter: React.FC = () => {
const branding = useSelector<ReduxState, BrandingOptions>((state) => state.branding)
return (
<Box mt="lg" mb="md" data-css="sidebar-footer">
{branding.withMadeWithLove && <MadeWithLove />}
</Box>
)
}
export default allowOverride(SidebarFooter, 'SidebarFooter')
export { SidebarFooter as OriginalSidebarFooter, SidebarFooter }
================================================
FILE: src/frontend/components/property-type/clean-property-component.tsx
================================================
import React, { FC, useMemo } from 'react'
import { BasePropertyProps } from './base-property-props.js'
import { BasePropertyComponent } from './base-property-component.js'
/**
* This component is the same as `BasePropertyComponent` but it will not render
* custom components. Use this in your custom components to render the default
* property component.
*
* This is useful if you want your custom component to appear custom only for
* specific `where` value and default for all others.
*/
const CleanPropertyComponent: FC<BasePropertyProps> = (props) => {
const { property } = props
const cleanProperty = useMemo(() => ({ ...property, components: {} }), [property])
return <BasePropertyComponent {...props} property={cleanProperty} />
}
export default CleanPropertyComponent
================================================
FILE: src/frontend/components/property-type/record-property-is-equal.ts
================================================
/* eslint-disable import/prefer-default-export */
import { EditPropertyProps, ShowPropertyProps } from './base-property-props.js'
/**
* Function used in React memo to compare if previous property value and next
* property value are the same.
*
* @private
*/
export const recordPropertyIsEqual = (
prevProps: Readonly<EditPropertyProps | ShowPropertyProps>,
nextProps: Readonly<EditPropertyProps | ShowPropertyProps>,
): boolean => {
const prevValue = prevProps.record.params[prevProps.property.path]
const nextValue = nextProps.record.params[nextProps.property.path]
const prevError = prevProps.record.errors[prevProps.property.path]
const nextError = nextProps.record.errors[nextProps.property.path]
return prevValue === nextValue && prevError === nextError
}
================================================
FILE: src/frontend/components/property-type/array/add-new-item-translation.tsx
================================================
import React from 'react'
import { Button, ButtonProps, Icon } from '@adminjs/design-system'
import { useTranslation } from '../../../hooks/index.js'
import { ResourceJSON, PropertyJSON } from '../../../interfaces/index.js'
type AddNewItemButtonProps = {
resource: ResourceJSON;
property: PropertyJSON;
} & ButtonProps
const AddNewItemButton: React.FC<AddNewItemButtonProps> = (props) => {
const { resource, property, ...btnProps } = props
const { translateProperty, translateButton } = useTranslation()
const label = translateProperty(
`${property.path}.addNewItem`,
resource.id,
{
defaultValue: translateButton('addNewItem', resource.id),
},
)
return (
<Button type="button" variant="outlined" {...btnProps}>
<Icon icon="Plus" />
{label}
</Button>
)
}
export default AddNewItemButton
================================================
FILE: src/frontend/components/property-type/array/index.ts
================================================
import Edit from './edit.js'
import List from './list.js'
import Show from './show.js'
export {
Show as show,
Edit as edit,
List as list,
}
================================================
FILE: src/frontend/components/property-type/array/list.tsx
================================================
import React from 'react'
import { useTranslation } from '../../../hooks/use-translation.js'
import { flat } from '../../../../utils/index.js'
import { ShowPropertyProps } from '../base-property-props.js'
import allowOverride from '../../../hoc/allow-override.js'
const List: React.FC<ShowPropertyProps> = (props) => {
const { property, record } = props
const values = flat.get(record.params, property.path) || []
const { translateProperty } = useTranslation()
return (
<span>{`${translateProperty('length')}: ${values.length}`}</span>
)
}
export default allowOverride(List, 'DefaultArrayListProperty')
================================================
FILE: src/frontend/components/property-type/boolean/boolean-property-value.tsx
================================================
import React from 'react'
import { Badge } from '@adminjs/design-system'
import { ShowPropertyProps } from '../base-property-props.js'
import { useTranslation } from '../../../hooks/index.js'
import mapValue from './map-value.js'
import allowOverride from '../../../hoc/allow-override.js'
const BooleanPropertyValue: React.FC<ShowPropertyProps> = (props) => {
const { record, property, resource } = props
const { tl } = useTranslation()
const rawValue = record?.params[property.path]
if (typeof rawValue === 'undefined' || rawValue === '') {
return null
}
const base = mapValue(rawValue)
const translation = tl(`${property.path}.${rawValue}`, resource.id, {
defaultValue: base,
})
return (
<Badge outline size="sm">{translation}</Badge>
)
}
export default allowOverride(BooleanPropertyValue, 'BooleanPropertyValue')
================================================
FILE: src/frontend/components/property-type/boolean/index.ts
================================================
import Edit from './edit.js'
import Show from './show.js'
import List from './list.js'
import Filter from './filter.js'
export {
Edit as edit,
Show as show,
List as list,
Filter as filter,
}
================================================
FILE: src/frontend/components/property-type/boolean/list.tsx
================================================
import React from 'react'
import BooleanPropertyValue from './boolean-property-value.js'
import { ShowPropertyProps } from '../base-property-props.js'
import allowOverride from '../../../hoc/allow-override.js'
const List: React.FC<ShowPropertyProps> = (props) => (
<BooleanPropertyValue {...props} />
)
export default allowOverride(List, 'DefaultBooleanListProperty')
================================================
FILE: src/frontend/components/property-type/boolean/map-value.tsx
================================================
export default (value): 'Yes' | 'No' | '' => {
if (typeof value === 'undefined') {
return ''
}
return value ? 'Yes' : 'No'
}
================================================
FILE: src/frontend/components/property-type/boolean/show.tsx
================================================
import React from 'react'
import { ValueGroup } from '@adminjs/design-system'
import BooleanPropertyValue from './boolean-property-value.js'
import { ShowPropertyProps } from '../base-property-props.js'
import allowOverride from '../../../hoc/allow-override.js'
import { useTranslation } from '../../../hooks/index.js'
const Show: React.FC<ShowPropertyProps> = (props) => {
const { property } = props
const { translateProperty } = useTranslation()
return (
<ValueGroup label={translateProperty(property.label, property.resourceId)}>
<BooleanPropertyValue {...props} />
</ValueGroup>
)
}
export default allowOverride(Show, 'DefaultBooleanShowProperty')
================================================
FILE: src/frontend/components/property-type/currency/filter.tsx
================================================
import { CurrencyInput, CurrencyInputProps, FormGroup } from '@adminjs/design-system'
import React, { FC } from 'react'
import { EditPropertyProps } from '../base-property-props.js'
import { PropertyLabel } from '../utils/property-label/index.js'
import allowOverride from '../../../hoc/allow-override.js'
const Filter: FC<EditPropertyProps> = (props) => {
const { onChange, property, filter } = props
const handleChange = (value) => {
onChange(property.path, value)
}
return (
<FormGroup variant="filter">
<PropertyLabel property={property} filter />
<CurrencyInput
id={property.path}
name={`filter-${property.path}`}
onValueChange={handleChange}
value={filter[property.path]}
{...property.props as CurrencyInputProps}
/>
</FormGroup>
)
}
export default allowOverride(Filter, 'DefaultCurrencyFilterProperty')
================================================
FILE: src/frontend/components/property-type/currency/format-value.ts
================================================
import { formatCurrencyProperty } from '@adminjs/design-system'
const optionsKeys: string[] = [
'value',
'decimalSeparator',
'groupSeparator',
'disableGroupSeparators',
'intlConfig',
'decimalScale',
'prefix',
'suffix',
]
const pickFormatOptions = (props: Record<string, string>) => {
const pickedProps = Object.keys(props).reduce((acc, curr) => {
if (optionsKeys.includes(curr as any)) {
if (props[curr] !== null && props[curr] !== undefined) {
acc[curr] = props[curr].toString()
}
}
return acc
}, {})
return pickedProps
}
const formatValue = (value: string, props: Record<string, string> = {}): string => {
const formatOptions = pickFormatOptions({ value, ...props })
return formatCurrencyProperty(formatOptions as any)
}
export default formatValue
================================================
FILE: src/frontend/components/property-type/currency/index.ts
================================================
import Edit from './edit.js'
import Filter from './filter.js'
import List from './list.js'
import Show from './show.js'
export {
Edit as edit,
Filter as filter,
List as list,
Show as show,
}
================================================
FILE: src/frontend/components/property-type/currency/list.tsx
================================================
import React from 'react'
import formatValue from './format-value.js'
import allowOverride from '../../../hoc/allow-override.js'
import { ShowPropertyProps } from '../base-property-props.js'
const List: React.FC<ShowPropertyProps> = (props) => {
const { property, record } = props
const value = formatValue(record.params[property.path], property.props)
return <span>{value}</span>
}
export default allowOverride(List, 'DefaultCurrencyListProperty')
================================================
FILE: src/frontend/components/property-type/currency/show.tsx
================================================
import { ValueGroup } from '@adminjs/design-system'
import React, { FC } from 'react'
import { EditPropertyProps } from '../base-property-props.js'
import formatValue from './format-value.js'
import allowOverride from '../../../hoc/allow-override.js'
import { useTranslation } from '../../../hooks/index.js'
const Show: FC<EditPropertyProps> = (props) => {
const { property, record } = props
const value = `${record.params[property.path]}`
const { translateProperty } = useTranslation()
return (
<ValueGroup label={translateProperty(property.label, property.resourceId)}>
{formatValue(value, property.props)}
</ValueGroup>
)
}
export default allowOverride(Show, 'DefaultCurrencyShowProperty')
================================================
FILE: src/frontend/components/property-type/datetime/index.ts
================================================
import Edit from './edit.js'
import Show from './show.js'
import List from './list.js'
import Filter from './filter.js'
export {
Edit as edit,
Show as show,
List as list,
Filter as filter,
}
================================================
FILE: src/frontend/components/property-type/datetime/list.tsx
================================================
import React from 'react'
import mapValue from './map-value.js'
import allowOverride from '../../../hoc/allow-override.js'
import { ShowPropertyProps } from '../base-property-props.js'
const List: React.FC<ShowPropertyProps> = (props) => {
const { property, record } = props
const value = mapValue(record.params[property.path], property.type)
return (
<span>{value}</span>
)
}
export default allowOverride(List, 'DefaultDatetimeListProperty')
================================================
FILE: src/frontend/components/property-type/datetime/map-value.ts
================================================
import { formatDateProperty } from '@adminjs/design-system'
import { PropertyType } from '../../../../backend/adapters/property/base-property.js'
import { stripTimeFromISO } from './strip-time-from-iso.js'
export default (value: Date, propertyType: PropertyType): string => {
if (!value) {
return ''
}
const date = propertyType === 'date' ? new Date(`${stripTimeFromISO(value)}T00:00:00`) : new Date(value)
if (date) {
return formatDateProperty(date, propertyType)
}
return ''
}
================================================
FILE: src/frontend/components/property-type/datetime/show.tsx
================================================
import React from 'react'
import { ValueGroup } from '@adminjs/design-system'
import allowOverride from '../../../hoc/allow-override.js'
import mapValue from './map-value.js'
import { ShowPropertyProps } from '../base-property-props.js'
import { useTranslation } from '../../../hooks/index.js'
const Show: React.FC<ShowPropertyProps> = (props) => {
const { property, record } = props
const { translateProperty } = useTranslation()
const value = mapValue(record.params[property.path], property.type)
return (
<ValueGroup label={translateProperty(property.label, property.resourceId)}>
{value}
</ValueGroup>
)
}
export default allowOverride(Show, 'DefaultDatetimeShowProperty')
================================================
FILE: src/frontend/components/property-type/datetime/strip-time-from-iso.ts
================================================
export const stripTimeFromISO = (date: string | Date | null): string | null => {
if (date === null) return null
if (typeof date === 'string') {
return date.replace(/T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, '')
}
return date.toISOString().replace(/T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, '')
}
================================================
FILE: src/frontend/components/property-type/default-type/index.ts
================================================
import Show from './show.js'
import Edit from './edit.js'
import Filter from './filter.js'
import List from './list.js'
export {
Show as show,
Edit as edit,
Filter as filter,
List as list,
}
================================================
FILE: src/frontend/components/property-type/default-type/list.tsx
================================================
import React from 'react'
import DefaultPropertyValue from './default-property-value.js'
import allowOverride from '../../../hoc/allow-override.js'
import { ShowPropertyProps } from '../base-property-props.js'
const List: React.FC<ShowPropertyProps> = (props) => (<DefaultPropertyValue {...props} />)
export default allowOverride(List, 'DefaultListProperty')
================================================
FILE: src/frontend/components/property-type/default-type/show.tsx
================================================
import React from 'react'
import { ValueGroup } from '@adminjs/design-system'
import allowOverride from '../../../hoc/allow-override.js'
import { ShowPropertyProps } from '../base-property-props.js'
import DefaultPropertyValue from './default-property-value.js'
import { useTranslation } from '../../../hooks/index.js'
const Show: React.FC<ShowPropertyProps> = (props) => {
const { property } = props
const { translateProperty } = useTranslation()
return (
<ValueGroup label={translateProperty(property.label, property.resourceId)}>
<DefaultPropertyValue {...props} />
</ValueGroup>
)
}
export default allowOverride(Show, 'DefaultShowProperty')
================================================
FILE: src/frontend/components/property-type/docs/on-property-change.doc.md
================================================
On change callback - It can take:
* one argument which is an entire {@link RecordJSON}
* 2 arguments - one __property.path__ and the second one: __value__.
* Used by the __edit__ and __filter__ components.
Let's take a look at an example of the edit component
It has one button: "Set Name". When this button is clicked - it triggers `onChange` callback
function. In this case, we are passing an updated record, so that we can change the value of another
property: `name`.
```javascript
import React from 'react'
import { Button, Box } from '@adminjs/design-system'
const ValueTrigger = (props) => {
const { onChange, record } = props
const handleClick = (): void => {
onChange({
...record,
params: {
...record.params,
name: 'my new name',
},
})
}
return (
<Box mb="xxl">
<Button type="button" onClick={handleClick}>Set Name</Button>
</Box>
)
}
export default ValueTrigger
```
================================================
FILE: src/frontend/components/property-type/key-value/index.ts
================================================
import Edit from './edit.js'
import Show from './show.js'
export {
Edit as edit,
Show as show,
}
================================================
FILE: src/frontend/components/property-type/mixed/convert-to-sub-property.ts
================================================
import { DELIMITER } from '../../../../utils/flat/constants.js'
import { PropertyJSON, BasePropertyJSON } from '../../../interfaces/index.js'
export function convertToSubProperty(
property: PropertyJSON,
subProperty: BasePropertyJSON,
): PropertyJSON {
const [subPropertyPath] = subProperty.name.split(DELIMITER).slice(-1)
return {
...subProperty,
path: [property.path, subPropertyPath].join(DELIMITER),
}
}
================================================
FILE: src/frontend/components/property-type/mixed/index.ts
================================================
import Edit from './edit.js'
import Show from './show.js'
import List from './list.js'
export {
Show as show,
Edit as edit,
List as list,
}
================================================
FILE: src/frontend/components/property-type/password/index.ts
================================================
/* eslint-disable import/prefer-default-export */
import Edit from './edit.js'
export {
Edit as edit,
}
================================================
FILE: src/frontend/components/property-type/phone/filter.tsx
================================================
import { PhoneInput, PhoneInputProps, FormGroup } from '@adminjs/design-system'
import React, { FC, useCallback } from 'react'
import { FilterPropertyProps } from '../base-property-props.js'
import { PropertyLabel } from '../utils/property-label/index.js'
import allowOverride from '../../../hoc/allow-override.js'
const Filter: FC<FilterPropertyProps> = (props) => {
const { onChange, property, filter } = props
const handleChange = useCallback((value) => {
onChange(property.path, value)
}, [])
return (
<FormGroup variant="filter">
<PropertyLabel property={property} filter />
<PhoneInput
id={property.path}
inputProps={{ name: `filter-${property.path}` }}
onChange={handleChange}
value={filter[property.path]}
{...property.props as PhoneInputProps}
/>
</FormGroup>
)
}
export default allowOverride(Filter, 'DefaultPhoneFilterProperty')
================================================
FILE: src/frontend/components/property-type/phone/index.ts
================================================
import Edit from './edit.js'
import Filter from './filter.js'
import List from './list.js'
import Show from './show.js'
export {
Edit as edit,
Filter as filter,
List as list,
Show as show,
}
================================================
FILE: src/frontend/components/property-type/phone/list.tsx
================================================
import React, { FC } from 'react'
import DefaultPropertyValue from '../default-type/default-property-value.js'
import allowOverride from '../../../hoc/allow-override.js'
import { ShowPropertyProps } from '../base-property-props.js'
const List: FC<ShowPropertyProps> = (props) => <DefaultPropertyValue {...props} />
export default allowOverride(List, 'DefaultPhoneListProperty')
================================================
FILE: src/frontend/components/property-type/phone/show.tsx
================================================
import React, { FC } from 'react'
import { ValueGroup } from '@adminjs/design-system'
import { ShowPropertyProps } from '../base-property-props.js'
import DefaultPropertyValue from '../default-type/default-property-value.js'
import allowOverride from '../../../hoc/allow-override.js'
import { useTranslation } from '../../../hooks/index.js'
const Show: FC<ShowPropertyProps> = (props) => {
const { property } = props
const { translateProperty } = useTranslation()
return (
<ValueGroup label={translateProperty(property.label, property.resourceId)}>
<DefaultPropertyValue {...props} />
</ValueGroup>
)
}
export default allowOverride(Show, 'DefaultPhoneShowProperty')
================================================
FILE: src/frontend/components/property-type/reference/index.ts
================================================
import Edit from './edit.js'
import Show from './show.js'
import List from './list.js'
import Filter from './filter.js'
export {
Edit as edit,
Show as show,
List as list,
Filter as filter,
}
================================================
FILE: src/frontend/components/property-type/reference/list.tsx
================================================
import React from 'react'
import ReferenceValue from './reference-value.js'
import { ShowPropertyProps } from '../base-property-props.js'
import allowOverride from '../../../hoc/allow-override.js'
const List: React.FC<ShowPropertyProps> = (props) => (
<ReferenceValue {...props} />
)
export default allowOverride(List, 'DefaultReferenceListProperty')
================================================
FILE: src/frontend/components/property-type/reference/show.tsx
================================================
import React from 'react'
import { ValueGroup } from '@adminjs/design-system'
import ReferenceValue from './reference-value.js'
import { ShowPropertyProps } from '../base-property-props.js'
import allowOverride from '../../../hoc/allow-override.js'
import { useTranslation } from '../../../hooks/index.js'
const Show: React.FC<ShowPropertyProps> = (props) => {
const { property, record } = props
const { translateProperty } = useTranslation()
return (
<ValueGroup label={translateProperty(property.label, property.resourceId)}>
<ReferenceValue
property={property}
record={record}
/>
</ValueGroup>
)
}
export default allowOverride(Show, 'DefaultReferenceShowProperty')
================================================
FILE: src/frontend/components/property-type/richtext/index.ts
================================================
import Edit from './edit.js'
import Show from './show.js'
import List from './list.js'
export {
Edit as edit,
Show as show,
List as list,
}
================================================
FILE: src/frontend/components/property-type/richtext/list.tsx
================================================
import truncate from 'lodash/truncate.js'
import React, { FC } from 'react'
import { ShowPropertyProps } from '../base-property-props.js'
import allowOverride from '../../../hoc/allow-override.js'
const stripHtml = (html: string): string => {
const el = window.document.createElement('DIV')
el.innerHTML = html
return el.textContent || el.innerText || ''
}
const List: FC<ShowPropertyProps> = (props) => {
const { property, record } = props
const maxLength = property.custom?.maxLength || 15
const value: string = record.params[property.path] || ''
const textValue = stripHtml(value)
return <>{truncate(textValue, { length: maxLength, separator: ' ' })}</>
}
export default allowOverride(List, 'DefaultReferenceListProperty')
================================================
FILE: src/frontend/components/property-type/richtext/show.tsx
================================================
import { Box, Text, ValueGroup } from '@adminjs/design-system'
import React, { FC } from 'react'
import xss from 'xss'
import { EditPropertyProps } from '../base-property-props.js'
import allowOverride from '../../../hoc/allow-override.js'
import { useTranslation } from '../../../hooks/index.js'
type InnerHtmlProp = {
__html: string;
}
const Show: FC<EditPropertyProps> = (props) => {
const { property, record } = props
const { translateProperty } = useTranslation()
const value: string = record.params[property.path] || ''
const createMarkup = (html: string): InnerHtmlProp => ({ __html: xss(html) })
return (
<ValueGroup label={translateProperty(property.label, property.resourceId)}>
<Box py="xl" px={['0', 'xl']} border="default">
<Text dangerouslySetInnerHTML={createMarkup(value)} />
</Box>
</ValueGroup>
)
}
export default allowOverride(Show, 'DefaultRichtextShowProperty')
================================================
FILE: src/frontend/components/property-type/textarea/index.ts
================================================
import Show from './show.js'
import Edit from './edit.js'
export {
Show as show,
Edit as edit,
}
================================================
FILE: src/frontend/components/property-type/textarea/show.tsx
================================================
import React from 'react'
import { ValueGroup } from '@adminjs/design-system'
import allowOverride from '../../../hoc/allow-override.js'
import { ShowPropertyProps } from '../base-property-props.js'
import { useTranslation } from '../../../hooks/index.js'
const Show: React.FC<ShowPropertyProps> = (props) => {
const { property, record } = props
const { translateProperty } = useTranslation()
const value = record.params[property.path] || ''
return (
<ValueGroup label={translateProperty(property.label, property.resourceId)}>
{value.split(/(?:\r\n|\r|\n)/g).map((line, i) => (
// eslint-disable-next-line react/no-array-index-key
<React.Fragment key={i}>
{line}
<br />
</React.Fragment>
))}
</ValueGroup>
)
}
export default allowOverride(Show, 'DefaultTextareaShowProperty')
================================================
FILE: src/frontend/components/property-type/utils/index.ts
================================================
export * from './property-label/index.js'
export * from './property-description/index.js'
================================================
FILE: src/frontend/components/property-type/utils/property-description/index.ts
================================================
export * from './property-description.js'
================================================
FILE: src/frontend/components/property-type/utils/property-label/index.ts
================================================
export * from './property-label.js'
================================================
FILE: src/frontend/components/routes/index.ts
================================================
export { default as DashboardRoute } from './dashboard.js'
export { default as RecordActionRoute } from './record-action.js'
export { default as ResourceActionRoute } from './resource-action.js'
export { default as BulkActionRoute } from './bulk-action.js'
export { default as PageRoute } from './page.js'
export { default as ResourceRoute } from './resource.js'
================================================
FILE: src/frontend/components/routes/utils/should-action-re-fetch-data.ts
================================================
import { RecordActionParams, BulkActionParams, ResourceActionParams } from '../../../../backend/utils/view-helpers/view-helpers.js'
type AnyActionParams = RecordActionParams & ResourceActionParams & BulkActionParams
/**
* Indicates if route action should be updated, meaning whether it should fetch
* new data from the backend.
* @private
*
* @param {AnyActionParams} currentMatchParams
* @param {AnyActionParams} newMatchParams
* @return {boolean}
*/
const shouldActionReFetchData = (
currentMatchParams: Partial<AnyActionParams>,
newMatchParams: Partial<AnyActionParams>,
): boolean => {
const {
resourceId,
recordId,
actionName,
} = currentMatchParams
const {
resourceId: newResourceId,
recordId: newRecordId,
actionName: newActionName,
} = newMatchParams
return resourceId !== newResourceId
|| recordId !== newRecordId
|| actionName !== newActionName
}
export default shouldActionReFetchData
================================================
FILE: src/frontend/components/spec/action-json.factory.ts
================================================
import { factory } from 'factory-girl'
import { ActionJSON } from '../../interfaces/index.js'
factory.define<ActionJSON>('ActionJSON', Object, {
actionType: 'record',
showInDrawer: true,
name: factory.sequence('ActionJSON.name', (n) => `action${n}`),
label: factory.sequence('ActionJSON.label', (n) => `action ${n}`),
showFilter: false,
showResourceActions: true,
resourceId: 'resource',
hideActionHeader: false,
containerWidth: 1,
layout: null,
variant: 'default',
parent: null,
hasHandler: true,
custom: {},
})
================================================
FILE: src/frontend/components/spec/factory.ts
================================================
// eslint-disable-next-line import/no-extraneous-dependencies
import { factory } from 'factory-girl'
import './action-json.factory.js'
import './page-json.factory.js'
import './property-json.factory.js'
import './record-json.factory.js'
import './resource-json.factory.js'
export default factory
================================================
FILE: src/frontend/components/spec/initialize-translations.ts
================================================
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
i18n.use(initReactI18next).init({
lng: 'en',
})
================================================
FILE: src/frontend/components/spec/page-json.factory.ts
================================================
import { factory } from 'factory-girl'
import { PageJSON } from '../../interfaces/index.js'
factory.define<PageJSON>('PageJSON', Object, {
name: factory.sequence('PageJSON.name', (n) => `page${n}`),
component: factory.sequence('PageJSON.component', (n) => `Component${n}`),
})
================================================
FILE: src/frontend/components/spec/property-json.factory.ts
================================================
import { factory } from 'factory-girl'
import { PropertyJSON } from '../../interfaces/index.js'
factory.define<PropertyJSON>('PropertyJSON', Object, {
custom: {},
isTitle: false,
isId: false,
isSortable: true,
availableValues: null,
label: factory.sequence('JSONProperty.label', (n) => `someProperty${n}`),
name: factory.sequence('JSONProperty.name', (n) => `someProperty${n}`),
position: factory.sequence('JSONProperty.position', (n) => n),
type: 'string',
reference: null,
isDisabled: false,
isArray: false,
isDraggable: false,
subProperties: [],
isRequired: true,
components: undefined,
path: factory.sequence('JSONProperty.path', (n) => `someProperty${n}`),
propertyPath: factory.sequence('JSONProperty.propertyPath', (n) => `someProperty${n}`),
resourceId: 'someResourceId',
isVirtual: false,
props: {},
hideLabel: false,
})
================================================
FILE: src/frontend/components/spec/test-context-provider.tsx
================================================
import React, { ReactNode } from 'react'
import { StaticRouter } from 'react-router-dom/server.js'
import { combineStyles } from '@adminjs/design-system'
// @ts-ignore Note: Ignore while @adminjs/design-system/styled-components doesn't export types
import { ThemeProvider } from '@adminjs/design-system/styled-components'
import { I18nextProvider } from 'react-i18next'
import { defaultLocale } from '../../../locale/index.js'
import initTranslations from '../../utils/adminjs.i18n.js'
const theme = combineStyles({})
type Props = {
children: ReactNode;
location?: string;
}
const TestContextProvider: React.FC<Props> = (props) => {
const { children, location } = props
const { i18n } = initTranslations(defaultLocale)
return (
<ThemeProvider theme={theme}>
<I18nextProvider i18n={i18n}>
<StaticRouter location={location || '/'}>
{children}
</StaticRouter>
</I18nextProvider>
</ThemeProvider>
)
}
export default TestContextProvider
================================================
FILE: src/frontend/hoc/index.ts
================================================
export * from './allow-override.js'
export * from './with-no-ssr.js'
export * from './with-notice.js'
================================================
FILE: src/frontend/hoc/with-no-ssr.tsx
================================================
import React, { ComponentType, useEffect, useState } from 'react'
/**
* A higher-order component that prevents a component from rendering server-side
*
* @template P - The props object of the wrapped component
* @param {React.ComponentType<P>} Component - The component to be wrapped
* @returns {React.FC<P>} A new component that renders the given component client-side only
*/
// eslint-disable-next-line max-len
const withNoSSR = <P extends Record<string, unknown>>(Component: ComponentType<P>) => (props: P) => {
const [isClient, setIsClient] = useState(false)
/**
* Sets isClient to true when the component is mounted on the client side
*/
useEffect(() => {
setIsClient(true)
}, [])
// Renders nothing if the component is not mounted on the client side
if (!isClient) return null
// Renders the wrapped component with the given props if it's mounted on the client side
return <Component {...props} />
}
export {
withNoSSR as default,
withNoSSR,
}
================================================
FILE: src/frontend/hooks/index.ts
================================================
export * from './use-action/index.js'
export * from './use-current-admin.js'
export * from './use-filter-drawer.js'
export * from './use-history-listen.js'
export * from './use-local-storage/index.js'
export * from './use-modal.js'
export * from './use-navigation-resources.js'
export * from './use-notice.js'
export * from './use-query-params.js'
export * from './use-record/index.js'
export * from './use-records/index.js'
export * from './use-resource/index.js'
export * from './use-selected-records/index.js'
export * from './use-translation.js'
================================================
FILE: src/frontend/hooks/use-filter-drawer.tsx
================================================
import { useDispatch, useSelector } from 'react-redux'
import { useEffect, useState } from 'react'
import { hideFilterDrawer, showFilterDrawer } from '../store/actions/filter-drawer.js'
import { ReduxState } from '../store/index.js'
import { useQueryParams } from './use-query-params.js'
export const useFilterDrawer = () => {
const [filtersCount, setFiltersCount] = useState(0)
const dispatch = useDispatch()
const isVisible = useSelector((state: ReduxState) => state.filterDrawer.isVisible)
const { filters = {} } = useQueryParams()
useEffect(() => {
setFiltersCount(Object.keys(filters).length)
}, [filters])
const toggleFilter = () => {
dispatch(isVisible ? hideFilterDrawer() : showFilterDrawer())
}
const open = () => {
dispatch(showFilterDrawer())
}
const close = () => {
dispatch(hideFilterDrawer())
}
return {
filtersCount,
isVisible,
toggleFilter,
open,
close,
}
}
================================================
FILE: src/frontend/hooks/use-notice.ts
================================================
import { useDispatch } from 'react-redux'
import { type NoticeMessage } from '../interfaces/noticeMessage.interface.js'
import { addNotice, type AddNoticeResponse } from '../store/actions/add-notice.js'
/**
* @memberof useNotice
* @alias AddNotice
*/
export type AddNotice = (notice: NoticeMessage) => AddNoticeResponse
/**
* @classdesc
* Hook which allows you to add notice message to the app.
*
* ```javascript
* import { useNotice, Button } from 'adminjs'
*
* const myComponent = () => {
* const sendNotice = useNotice()
* return (
* <Button onClick={() => sendNotice({ message: 'I am awesome' })}>I am awesome</Button>
* )
* }
* ```
*
* @class
* @subcategory Hooks
* @bundle
* @hideconstructor
*/
export const useNotice = (): AddNotice => {
const dispatch = useDispatch()
return (notice) => dispatch(addNotice(notice))
}
export default useNotice
================================================
FILE: src/frontend/hooks/use-action/index.ts
================================================
export * from './use-action.js'
export * from './use-action-response-handler.js'
export * from './use-action.types.js'
================================================
FILE: src/frontend/hooks/use-action/use-action-response-handler.ts
================================================
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { useNavigate, useLocation } from 'react-router'
import { ActionResponse } from '../../../backend/actions/action.interface.js'
import { appendForceRefresh } from '../../components/actions/utils/append-force-refresh.js'
import { ActionCallCallback } from './index.js'
import { useNotice } from '../use-notice.js'
export const useActionResponseHandler = (onActionCall?: ActionCallCallback) => {
const location = useLocation()
const navigate = useNavigate()
const addNotice = useNotice()
return (response: ActionResponse) => {
const { data } = response
if (data.notice) {
addNotice(data.notice)
}
if (data.redirectUrl && location.pathname !== data.redirectUrl) {
const appended = appendForceRefresh(data.redirectUrl)
navigate(appended)
}
if (onActionCall) {
onActionCall(data)
}
}
}
================================================
FILE: src/frontend/hooks/use-action/use-action.doc.md
================================================
The hook which allows you to use {@link ActionJSON} to perform actual actions on the backend.
Base on the action type and parameters (like {@link ActionJSON.guard}) it behaves differently.
### Usage
```javascript
import { useAction } from 'adminjs'
import { Button } from '@adminjs/design-system'
const myComponent = ({ action }) => {
const { href, handleClick } = useAction(action, {
resourceId, recordId, recordIds,
}, actionPerformed)
return (
<Button as="a" onClick={handleClick} href={href}>Click this action</Button>
)
}
```
================================================
FILE: src/frontend/hooks/use-local-storage/index.ts
================================================
export * from './use-local-storage.js'
export * from './use-local-storage-result.type.js'
================================================
FILE: src/frontend/hooks/use-local-storage/use-local-storage-result.type.ts
================================================
import React from 'react'
export type UseLocalStorageResult<T> = [
T,
React.Dispatch<React.SetStateAction<T>>
];
/**
* Result of the {@link useLocalStorage}.
* It is a tuple containing value and the setter
*
* @typedef {Array} UseLocalStorageResult
* @memberof useLocalStorage
* @alias UseLocalStorageResult
* @property {T} [0] the value stored in the local store
* @property {React.Dispatch<React.SetStateAction<T>>} [1] value setter compatible with react
* useState
*/
================================================
FILE: src/frontend/hooks/use-local-storage/use-local-storage.doc.md
================================================
The hook which allows you to store particular data into local storage.
It works very similar to `useState` with the exception that it requires the key under which data
will be stored.
### Usage
```javascript
import { useLocalStorage } from 'adminjs'
const MyRecordActionComponent = (props) => {
const [isOpen, setIsOpen] = useLocalStorage('isSidebarOpen', false)
// ....
return (
<Box>
{ isOpen ? (
<Drawer>
Drawer content
</Drawer>
) : ''}
</Box>
)
}
export default MyRecordActionComponent
```
Returns {@link UseRecordResult}.
================================================
FILE: src/frontend/hooks/use-record/filter-record.ts
================================================
import { flat } from '../../../utils/flat/index.js'
import { RecordJSON } from '../../interfaces/index.js'
import { UseRecordOptions } from './use-record.type.js'
export const filterRecordParams = function<T extends RecordJSON> (
record: T,
options: UseRecordOptions = {},
): T {
if (options.includeParams && record) {
return {
...record,
params: flat.selectParams(record.params || {}, options.includeParams),
}
}
return record
}
export const isPropertyPermitted = (propertyName, options: UseRecordOptions = {}): boolean => {
const { includeParams } = options
if (includeParams) {
const parts = flat.pathToParts(propertyName, { skipArrayIndexes: true })
return parts.some((part) => includeParams.includes(part))
}
return true
}
================================================
FILE: src/frontend/hooks/use-record/index.ts
================================================
export * from './use-record.js'
export * from './use-record.type.js'
export * from './is-entire-record-given.js'
export * from './merge-record-response.js'
export * from './params-to-form-data.js'
export * from './update-record.js'
================================================
FILE: src/frontend/hooks/use-record/is-entire-record-given.ts
================================================
import { RecordJSON } from '../../interfaces/index.js'
const isEntireRecordGiven = (
propertyOrRecord: RecordJSON | string,
value?: string,
): boolean => !!(typeof value === 'undefined'
// user can pass property and omit value. This makes sense when
// third argument of the function (selectedRecord) is passed to onChange
// callback
&& !(typeof propertyOrRecord === 'string')
// we assume that only params has to be given
&& propertyOrRecord.params)
export {
isEntireRecordGiven as default,
isEntireRecordGiven,
}
================================================
FILE: src/frontend/hooks/use-record/merge-record-response.ts
================================================
import { RecordJSON } from '../../interfaces/index.js'
import { RecordActionResponse } from '../../../backend/actions/action.interface.js'
/**
* Handlers of all [Actions]{@link Action} of type `record` returns record.
* Depending on a place and response we have to merge what was returned
* to the actual state. It is done in following places:
* - {@link useRecord} hook
* - {@link RecordInList} component
* - {@link RecordAction} component
*
* @private
*/
const mergeRecordResponse = (record: RecordJSON, response: RecordActionResponse): RecordJSON => ({
// we start from the response because it can have different recordActions or bulkActions
...(response.record || record),
// records has to be reset every time because so that user wont
// see old errors which are not relevant anymore
errors: response.record.errors,
populated: { ...record.populated, ...response.record.populated },
params: { ...record.params, ...response.record.params },
})
export default mergeRecordResponse
================================================
FILE: src/frontend/hooks/use-records/index.ts
================================================
export * from './use-records.js'
export * from './use-records-result.type.js'
================================================
FILE: src/frontend/hooks/use-records/use-records-result.type.ts
================================================
import { AxiosResponse } from 'axios'
import { ListActionResponse } from '../../../backend/index.js'
import { RecordJSON } from '../../interfaces/index.js'
/**
* Result of the {@link useRecords} hook.
* It is a object containing multiple tools you can use in your component
* @memberof useRecords
* @alias UseRecordsResult
*/
export type UseRecordsResult = {
/**
* Array of records fetched from the backend
*/
records: Array<RecordJSON>;
/** loading state */
loading: boolean;
/** current page (in pagination) */
page: number;
/** perPage limit returned by the backend */
perPage: number;
/** total number of pages in for current query */
total: number;
/** sort direction */
direction: 'asc' | 'desc';
/** field used as a sortBy column */
sortBy?: string;
/** function which triggers fetching the data */
fetchData: () => Promise<AxiosResponse<ListActionResponse>>;
}
================================================
FILE: src/frontend/hooks/use-resource/index.ts
================================================
export * from './use-resource.js'
================================================
FILE: src/frontend/hooks/use-resource/use-resource.doc.md
================================================
The hook which allows you to get {@link ResourceJSON} object for a particular resource ID from the store.
### Usage
```javascript
import { useResource } from 'adminjs'
const MyRecordActionComponent = (props) => {
const UsersResource = useResource('Users')
const { properties } = UsersResource
// ....
}
export default MyRecordActionComponent
```
================================================
FILE: src/frontend/hooks/use-resource/use-resource.ts
================================================
import { useSelector } from 'react-redux'
import { ResourceJSON } from '../../interfaces/resource-json.interface.js'
import { ReduxState } from '../../store/store.js'
/**
* @load ./use-resource.doc.md
* @subcategory Hooks
* @class
* @hideconstructor
* @bundle
* @param {string} resourceId Id of a resource you want to get
*/
const useResource = (resourceId: string): ResourceJSON | undefined => {
const resources = useSelector((state: ReduxState) => state.resources)
const foundResource = resources.find((resource) => resource.id === resourceId)
return foundResource
}
export {
useResource as default,
useResource,
}
================================================
FILE: src/frontend/hooks/use-selected-records/index.ts
================================================
export * from './use-selected-records.js'
export * from './use-selected-records-result.type.js'
================================================
FILE: src/frontend/hooks/use-selected-records/use-selected-records-result.type.ts
================================================
import { RecordJSON } from '../../interfaces/index.js'
/**
* Result of the {@link useSelectedRecords} hook.
* It is a object containing multiple tools you can use in your component
* @memberof useSelectedRecords
* @alias UseSelectedRecordsResult
*/
export type UseSelectedRecordsResult = {
/** Array of selected records */
selectedRecords: Array<RecordJSON>;
/** Sets selected records */
setSelectedRecords: (records: Array<RecordJSON>) => void;
/** handler function for single select action */
handleSelect: (record: RecordJSON) => void;
/** handler function for `select all records` action */
handleSelectAll: () => void;
}
================================================
FILE: src/frontend/interfaces/index.ts
================================================
export * from './action/index.js'
export * from './modal.interface.js'
export * from './noticeMessage.interface.js'
export * from './page-json.interface.js'
export * from './property-json/index.js'
export * from './record-json.interface.js'
export * from './resource-json.interface.js'
================================================
FILE: src/frontend/interfaces/modal.interface.ts
================================================
import type { ModalProps } from '@adminjs/design-system'
import { SHOW_MODAL, HIDE_MODAL } from '../store/index.js'
export interface ModalData {
modalProps: ModalProps;
type?: 'alert' | 'confirm';
resourceId?: string;
confirmAction?: () => void;
}
export type ModalFunctions = {
openModal: (data: ModalData) => void
closeModal: () => void
}
export type ShowModalResponse = {
type: typeof SHOW_MODAL
data: ModalData;
}
export type HideModalResponse = {
type: typeof HIDE_MODAL;
}
================================================
FILE: src/frontend/interfaces/noticeMessage.interface.ts
================================================
import { type MessageBoxProps } from '@adminjs/design-system'
import { type TOptions } from 'i18next'
import { type ReactNode } from 'react'
/**
* NoticeMessage which can be presented as a "Toast" message.
* @alias NoticeMessage
*/
export type NoticeMessage = {
message: string
// Extra 'error' to handle backwards. Error is replaced to danger in notification box
type?: MessageBoxProps['variant'] | 'error'
options?: TOptions
resourceId?: string
body?: ReactNode
}
================================================
FILE: src/frontend/interfaces/page-json.interface.ts
================================================
import type { IconProps } from '@adminjs/design-system'
/**
* Representing the page in the sidebar
* @subcategory Frontend
*/
export interface PageJSON {
/**
* Page name
*/
name: string
/**
* Page component. Bundled with {@link ComponentLoader}
*/
component: string
/**
* Page icon
*/
icon?: IconProps['icon']
}
================================================
FILE: src/frontend/interfaces/action/action-has-component.ts
================================================
import { ActionJSON } from './action-json.interface.js'
export const actionHasDisabledComponent = (action: ActionJSON): boolean => (
typeof action.component !== 'undefined' && action.component === false
)
export const actionHasCustomComponent = (action: ActionJSON): boolean => (
typeof action.component === 'string'
)
================================================
FILE: src/frontend/interfaces/action/build-action-test-id.ts
================================================
import { ActionJSON } from './action-json.interface.js'
export const buildActionTestId = (action: ActionJSON): string => `action-${action.name}`
================================================
FILE: src/frontend/interfaces/action/index.ts
================================================
export * from './action-has-component.js'
export * from './action-href.js'
export * from './action-json.interface.js'
export * from './build-action-api-call-trigger.js'
export * from './build-action-test-id.js'
export * from './build-action-click-handler.js'
export * from './call-action-api.js'
export * from './is-bulk-action.js'
export * from './is-resource-action.js'
export * from './is-record-action.js'
================================================
FILE: src/frontend/interfaces/action/is-bulk-action.ts
================================================
import { BulkActionParams } from '../../../backend/utils/view-helpers/view-helpers.js'
import { ActionJSON } from '../action/index.js'
import { DifferentActionParams } from '../../hooks/use-action/use-action.types.js'
export const isBulkAction = (
params: DifferentActionParams,
action: ActionJSON,
): params is BulkActionParams => 'recordIds' in params && action.actionType === 'bulk'
================================================
FILE: src/frontend/interfaces/action/is-record-action.ts
================================================
import { RecordActionParams } from '../../../backend/utils/view-helpers/view-helpers.js'
import { ActionJSON } from '../action/index.js'
import { DifferentActionParams } from '../../hooks/use-action/use-action.types.js'
export const isRecordAction = (
params: DifferentActionParams,
action: ActionJSON,
): params is RecordActionParams => (
'recordId' in params && action.actionType === 'record'
)
================================================
FILE: src/frontend/interfaces/action/is-resource-action.ts
================================================
import { ResourceActionParams } from '../../../backend/utils/view-helpers/view-helpers.js'
import { ActionJSON } from '../action/index.js'
import { DifferentActionParams } from '../../hooks/use-action/use-action.types.js'
export const isResourceAction = (
params: DifferentActionParams,
action: ActionJSON,
): params is ResourceActionParams => 'recordIds' in params && action.actionType === 'resource'
================================================
FILE: src/frontend/interfaces/property-json/index.ts
================================================
export * from './property-json.interface.js'
================================================
FILE: src/frontend/store/index.ts
================================================
export * from './actions/index.js'
export * from './initialize-store.js'
export * from './reducers/index.js'
export * from './store.js'
export { default as createStore } from './store.js'
================================================
FILE: src/frontend/store/actions/add-notice.ts
================================================
import { type NoticeMessage } from '../../interfaces/noticeMessage.interface.js'
import { type NoticeMessageInState } from '../reducers/noticesReducer.js'
export const ADD_NOTICE = 'ADD_NOTICE'
export type AddNoticeResponse = {
type: typeof ADD_NOTICE
data: NoticeMessageInState
}
export const addNotice = (data: NoticeMessage): AddNoticeResponse => ({
type: ADD_NOTICE,
data: {
id: `notice-${Date.now() + Math.random()}`,
progress: 0,
...data,
},
})
================================================
FILE: src/frontend/store/actions/drop-notice.ts
================================================
export const DROP_NOTICE = 'DROP_NOTICE'
export type DropNoticeResponse = {
type: typeof DROP_NOTICE
data: {
noticeId: string
}
}
export const dropNotice = (noticeId: string): DropNoticeResponse => ({
type: 'DROP_NOTICE',
data: { noticeId },
})
================================================
FILE: src/frontend/store/actions/filter-drawer.ts
================================================
export const OPEN_FILTER_DRAWER = 'OPEN_FILTER_DRAWER'
export const CLOSE_FILTER_DRAWER = 'CLOSE_FILTER_DRAWER'
export type FilterDrawerAction =
| { type: typeof OPEN_FILTER_DRAWER; isVisible: true }
| { type: typeof CLOSE_FILTER_DRAWER; isVisible: false }
export const showFilterDrawer = (): FilterDrawerAction => ({
type: OPEN_FILTER_DRAWER,
isVisible: true,
})
export const hideFilterDrawer = (): FilterDrawerAction => ({
type: CLOSE_FILTER_DRAWER,
isVisible: false,
})
================================================
FILE: src/frontend/store/actions/index.ts
================================================
export * from './add-notice.js'
export * from './drop-notice.js'
export * from './initialize-assets.js'
export * from './initialize-branding.js'
export * from './initialize-dashboard.js'
export * from './initialize-locale.js'
export * from './initialize-pages.js'
export * from './initialize-paths.js'
export * from './initialize-resources.js'
export * from './initialize-theme.js'
export * from './initialize-versions.js'
export * from './modal.js'
export * from './route-changed.js'
export * from './set-current-admin.js'
export * from './set-drawer-preroute.js'
export * from './set-notice-progress.js'
================================================
FILE: src/frontend/store/actions/initialize-assets.ts
================================================
import type { Assets } from '../../../adminjs-options.interface.js'
export const ASSETS_INITIALIZE = 'ASSETS_INITIALIZE'
export type initializeAssetsResponse = {
type: string;
data: Assets;
}
export const initializeAssets = (data: Assets): initializeAssetsResponse => ({
type: ASSETS_INITIALIZE,
data,
})
================================================
FILE: src/frontend/store/actions/initialize-branding.ts
================================================
import type { BrandingOptions } from '../../../adminjs-options.interface.js'
export const BRANDING_INITIALIZE = 'BRANDING_INITIALIZE'
export type InitializeBrandingResponse = {
type: typeof BRANDING_INITIALIZE
data: BrandingOptions
}
export const initializeBranding = (data: BrandingOptions): InitializeBrandingResponse => ({
type: BRANDING_INITIALIZE,
data,
})
================================================
FILE: src/frontend/store/actions/initialize-dashboard.ts
================================================
import { DashboardInState } from '../reducers/dashboardReducer.js'
export const DASHBOARD_INITIALIZE = 'DASHBOARD_INITIALIZE'
export type InitializeDashboardResponse = {
type: typeof DASHBOARD_INITIALIZE
data: DashboardInState
}
export const initializeDashboard = (data: DashboardInState): InitializeDashboardResponse => ({
type: DASHBOARD_INITIALIZE,
data,
})
================================================
FILE: src/frontend/store/actions/initialize-locale.ts
================================================
import { Locale } from '../../../locale/config.js'
export const LOCALE_INITIALIZE = 'LOCALE_INITIALIZE'
export type InitializeLocaleResponse = {
type: typeof LOCALE_INITIALIZE;
data: Locale;
}
export const initializeLocale = (data: Locale): InitializeLocaleResponse => ({
type: LOCALE_INITIALIZE,
data,
})
================================================
FILE: src/frontend/store/actions/initialize-pages.ts
================================================
import { AdminPage } from '../../../adminjs-options.interface.js'
export const PAGES_INITIALIZE = 'PAGES_INITIALIZE'
export type InitializePagesResponse = {
type: typeof PAGES_INITIALIZE;
data: Array<AdminPage>;
}
export const initializePages = (data: Array<AdminPage>): InitializePagesResponse => ({
type: PAGES_INITIALIZE,
data,
})
================================================
FILE: src/frontend/store/actions/initialize-paths.ts
================================================
import type { PathsInState } from '../reducers/pathsReducer.js'
export const PATHS_INITIALIZE = 'PATHS_INITIALIZE'
export type InitializePathsResponse = {
type: typeof PATHS_INITIALIZE
data: PathsInState
}
export const initializePaths = (data: PathsInState): InitializePathsResponse => ({
type: PATHS_INITIALIZE,
data,
})
================================================
FILE: src/frontend/store/actions/initialize-resources.ts
================================================
import { ResourceJSON } from '../../interfaces/index.js'
export const RESOURCES_INITIALIZE = 'RESOURCES_INITIALIZE'
export type InitializeResourcesResponse = {
type: typeof RESOURCES_INITIALIZE;
data: Array<ResourceJSON>;
}
export const initializeResources = (data: Array<ResourceJSON>): InitializeResourcesResponse => ({
type: RESOURCES_INITIALIZE,
data,
})
================================================
FILE: src/frontend/store/actions/initialize-theme.ts
================================================
import type { ThemeInState } from '../reducers/themeReducer.js'
export const THEME_INITIALIZE = 'THEME_INITIALIZE'
export type initializeThemeResponse = {
type: typeof THEME_INITIALIZE
data: ThemeInState
}
export const initializeTheme = (data: ThemeInState): initializeThemeResponse => ({
type: THEME_INITIALIZE,
data,
})
================================================
FILE: src/frontend/store/actions/initialize-versions.ts
================================================
import { VersionProps } from '../../../adminjs-options.interface.js'
export const VERSIONS_INITIALIZE = 'VERSIONS_INITIALIZE'
export type InitializeVersionsResponse = {
type: typeof VERSIONS_INITIALIZE
data: VersionProps
}
export const initializeVersions = (data: VersionProps): InitializeVersionsResponse => ({
type: VERSIONS_INITIALIZE,
data,
})
================================================
FILE: src/frontend/store/actions/modal.ts
================================================
import type { ModalData, ShowModalResponse, HideModalResponse } from '../../interfaces/index.js'
export const SHOW_MODAL = 'SHOW_MODAL'
export const HIDE_MODAL = 'HIDE_MODAL'
export const showModal = (data: ModalData): ShowModalResponse => ({
type: SHOW_MODAL,
data,
})
export const hideModal = (): HideModalResponse => ({
type: HIDE_MODAL,
})
================================================
FILE: src/frontend/store/actions/route-changed.ts
================================================
import type { useLocation } from 'react-router'
export const INITIAL_ROUTE = 'INITIAL_ROUTE'
export const ROUTE_CHANGED = 'ROUTE_CHANGED'
export type RouteChangedResponse = {
type: typeof ROUTE_CHANGED
data: any
}
export const initializeRoute = (
location: Partial<ReturnType<typeof useLocation>>,
): RouteChangedResponse => ({
type: ROUTE_CHANGED,
data: location,
})
export const changeRoute = (location: ReturnType<typeof useLocation>): RouteChangedResponse => ({
type: ROUTE_CHANGED,
data: location,
})
================================================
FILE: src/frontend/store/actions/set-current-admin.ts
================================================
import type { SessionInState } from '../reducers/sessionReducer.js'
export const SESSION_INITIALIZE = 'SESSION_INITIALIZE'
export type SetCurrentAdminResponse = {
type: typeof SESSION_INITIALIZE
data: SessionInState
}
export const setCurrentAdmin = (data: SessionInState = null): SetCurrentAdminResponse => ({
type: SESSION_INITIALIZE,
data,
})
================================================
FILE: src/frontend/store/actions/set-drawer-preroute.ts
================================================
import type { useLocation } from 'react-router'
export const DRAWER_PREROUTE_SET = 'DRAWER_PREROUTE_SET'
export type SetDrawerPreRouteResponse = {
type: typeof DRAWER_PREROUTE_SET
data: {
previousRoute: Partial<ReturnType<typeof useLocation>> | null
}
}
export const setDrawerPreRoute = (data: {
previousRoute: Partial<ReturnType<typeof useLocation>> | null
}): SetDrawerPreRouteResponse => ({
type: DRAWER_PREROUTE_SET,
data,
})
================================================
FILE: src/frontend/store/actions/set-notice-progress.ts
================================================
export const SET_NOTICE_PROGRESS = 'SET_NOTICE_PROGRESS'
export type SetNoticeProgress = {
noticeId: string;
progress: number;
}
export type SetNoticeProgressResponse = {
type: typeof SET_NOTICE_PROGRESS;
data: SetNoticeProgress;
}
export const setNoticeProgress = (data: SetNoticeProgress): SetNoticeProgressResponse => ({
type: SET_NOTICE_PROGRESS,
data,
})
================================================
FILE: src/frontend/store/reducers/assetsReducer.ts
================================================
import type { Assets } from '../../../adminjs-options.interface.js'
import { ASSETS_INITIALIZE } from '../actions/initialize-assets.js'
export const assetsReducer = (
state = {},
action: {
type: string
data: Assets
},
) => {
switch (action.type) {
case ASSETS_INITIALIZE:
return action.data
default:
return state
}
}
================================================
FILE: src/frontend/store/reducers/brandingReducer.ts
================================================
import type { BrandingOptions } from '../../../adminjs-options.interface.js'
import { BRANDING_INITIALIZE } from '../actions/initialize-branding.js'
export const brandingReducer = (
state = {},
action: {
type: string
data: BrandingOptions
},
) => {
switch (action.type) {
case BRANDING_INITIALIZE:
return action.data
default:
return state
}
}
================================================
FILE: src/frontend/store/reducers/dashboardReducer.ts
================================================
import { DASHBOARD_INITIALIZE } from '../actions/initialize-dashboard.js'
export type DashboardInState = {
component?: string
}
export const dashboardReducer = (
state = {},
action: {
type: string
data: DashboardInState
},
): DashboardInState => {
switch (action.type) {
case DASHBOARD_INITIALIZE:
return action.data
default:
return state
}
}
================================================
FILE: src/frontend/store/reducers/drawerReducer.ts
================================================
import { DRAWER_PREROUTE_SET, SetDrawerPreRouteResponse } from '../actions/set-drawer-preroute.js'
export type DrawerInState = SetDrawerPreRouteResponse['data']
export const drawerReducer = (
state: DrawerInState = { previousRoute: null },
action: {
type: string
data: DrawerInState
},
) => {
switch (action.type) {
case DRAWER_PREROUTE_SET: {
return {
...state,
...action.data,
}
}
default: {
return state
}
}
}
================================================
FILE: src/frontend/store/reducers/filterDrawerReducer.ts
================================================
import {
CLOSE_FILTER_DRAWER,
OPEN_FILTER_DRAWER,
type FilterDrawerAction,
} from '../actions/filter-drawer.js'
export type FilterDrawerInState = ReturnType<typeof filterDrawerReducer>
const initialState = {
isVisible: false,
}
export const filterDrawerReducer = (state = initialState, action: FilterDrawerAction) => {
switch (action.type) {
case OPEN_FILTER_DRAWER: {
return { ...state, isVisible: action.isVisible }
}
case CLOSE_FILTER_DRAWER: {
return { ...state, isVisible: action.isVisible }
}
default: {
return state
}
}
}
================================================
FILE: src/frontend/store/reducers/index.ts
================================================
export * from './assetsReducer.js'
export * from './brandingReducer.js'
export * from './dashboardReducer.js'
export * from './drawerReducer.js'
export * from './filterDrawerReducer.js'
export * from './localesReducer.js'
export * from './modalReducer.js'
export * from './noticesReducer.js'
export * from './pagesReducer.js'
export * from './pathsReducer.js'
export * from './resourcesReducer.js'
export * from './routerReducer.js'
export * from './sessionReducer.js'
export * from './themeReducer.js'
export * from './versionsReducer.js'
================================================
FILE: src/frontend/store/reducers/localesReducer.ts
================================================
import type { Locale } from '../../../locale/config.js'
import { LOCALE_INITIALIZE } from '../actions/initialize-locale.js'
export type LolcaleInState = Locale
const defaultLocale = { language: 'en', translations: {} } as Locale
export const localesReducer = (
state: Locale = defaultLocale,
action: {
type: string
data: Locale
},
) => {
switch (action.type) {
case LOCALE_INITIALIZE:
return action.data
default:
return state
}
}
================================================
FILE: src/frontend/store/reducers/modalReducer.ts
================================================
import type { ModalData } from '../../interfaces/index.js'
import { HIDE_MODAL, SHOW_MODAL } from '../actions/modal.js'
export type ModalInState = (ModalData & { show: true }) | { show: false }
export const modalReducer = (
state: ModalInState = { show: false },
action: {
type: string
data: ModalData
},
): ModalInState => {
switch (action.type) {
case SHOW_MODAL: {
return {
...action.data,
show: true,
}
}
case HIDE_MODAL: {
return { show: false }
}
default:
return state
}
}
================================================
FILE: src/frontend/store/reducers/pagesReducer.ts
================================================
import type { PageJSON } from '../../interfaces/page-json.interface.js'
import { PAGES_INITIALIZE } from '../actions/initialize-pages.js'
export type PagesInState = Array<PageJSON>
export const pagesReducer = (
state: PagesInState = [],
action: {
type: string
data: PagesInState
},
) => {
switch (action.type) {
case PAGES_INITIALIZE:
return action.data
default:
return state
}
}
================================================
FILE: src/frontend/store/reducers/pathsReducer.ts
================================================
import { DEFAULT_PATHS } from '../../../constants.js'
import { PATHS_INITIALIZE } from '../actions/initialize-paths.js'
export type PathsInState = {
rootPath: string;
logoutPath: string;
loginPath: string;
assetsCDN?: string;
};
export const pathsReducer = (
state: PathsInState = DEFAULT_PATHS,
action: {
type: string;
data: PathsInState;
},
): PathsInState => {
switch (action.type) {
case PATHS_INITIALIZE:
return action.data
default:
return state
}
}
================================================
FILE: src/frontend/store/reducers/resourcesReducer.ts
================================================
import type { ResourceJSON } from '../../interfaces/resource-json.interface.js'
import { RESOURCES_INITIALIZE } from '../actions/initialize-resources.js'
export type ResourcesInState = Array<ResourceJSON>
export const resourcesReducer = (
state: ResourcesInState = [],
action: {
type: string
data: ResourcesInState
},
) => {
switch (action.type) {
case RESOURCES_INITIALIZE:
return action.data
default:
return state
}
}
================================================
FILE: src/frontend/store/reducers/routerReducer.ts
================================================
import type { Location } from 'react-router'
import { ROUTE_CHANGED, INITIAL_ROUTE } from '../actions/route-changed.js'
export type RouterInState = {
from: Partial<Location>
to: Partial<Location>
}
export const routerReducer = (
state: RouterInState = { from: {}, to: {} },
action: {
type: typeof INITIAL_ROUTE | typeof ROUTE_CHANGED
data: any
},
) => {
switch (action.type) {
case INITIAL_ROUTE:
return {
...state,
from: { ...action.data },
}
case ROUTE_CHANGED:
return {
from: { ...state.to },
to: { ...action.data },
}
default:
return state
}
}
================================================
FILE: src/frontend/store/reducers/sessionReducer.ts
================================================
import type { CurrentAdmin } from '../../../current-admin.interface.js'
import { SESSION_INITIALIZE } from '../actions/set-current-admin.js'
export type SessionInState = CurrentAdmin | null
export const sessionReducer = (
state: SessionInState = null,
action: {
type: string
data: SessionInState
},
) => {
switch (action.type) {
case SESSION_INITIALIZE:
return action.data
default:
return state
}
}
================================================
FILE: src/frontend/store/reducers/themeReducer.ts
================================================
import type { ThemeConfig } from '../../../adminjs-options.interface.js'
import { THEME_INITIALIZE } from '../actions/initialize-theme.js'
export type ThemeInState = (ThemeConfig & { availableThemes?: ThemeConfig[] }) | null
export const themeReducer = (
state: ThemeInState = null,
action: {
type: string
data: ThemeInState
},
) => {
switch (action.type) {
case THEME_INITIALIZE:
return action.data
default:
return state
}
}
================================================
FILE: src/frontend/store/reducers/versionsReducer.ts
================================================
import { VersionProps } from '../../../adminjs-options.interface.js'
import { VERSIONS_INITIALIZE } from '../actions/initialize-versions.js'
export const versionsReducer = (
state = {},
action: {
type: string;
data: VersionProps;
},
) => {
switch (action.type) {
case VERSIONS_INITIALIZE:
return {
admin: action.data.admin,
app: action.data.app,
}
default:
return state
}
}
================================================
FILE: src/frontend/store/utils/pages-to-store.ts
================================================
/* eslint-disable implicit-arrow-linebreak */
import type { AdminJSOptions } from '../../../adminjs-options.interface.js'
import type { PageJSON } from '../../interfaces/index.js'
export const pagesToStore = (pages: AdminJSOptions['pages'] = {}): Array<PageJSON> =>
Object.entries(pages).map(([key, adminPage]) => ({
name: key,
component: adminPage.component,
icon: adminPage.icon,
}))
================================================
FILE: src/frontend/utils/data-css-name.ts
================================================
/* eslint-disable max-len */
export const getDataCss = (...args: (string | number)[]) => args.join('-')
export const getResourceElementCss = (resourceId: string, suffix: string) => getDataCss(resourceId, suffix)
export const getActionElementCss = (resourceId: string, actionName: string, suffix: string) => getDataCss(resourceId, actionName, suffix)
================================================
FILE: src/frontend/utils/index.ts
================================================
export * from './api-client.js'
export * from './overridable-component.js'
export * from './data-css-name.js'
================================================
FILE: src/locale/default-config.ts
================================================
import type { InitOptions } from 'i18next'
import startCase from 'lodash/startCase.js'
import type { Locale } from './config.js'
const DEFAULT_LOAD = 'currentOnly'
export const DEFAULT_NS = 'translation'
export const defaultLocale: Locale = {
language: 'en',
translations: {},
availableLanguages: ['en'],
}
export const defaultConfig: InitOptions = {
debug: process.env.NODE_ENV === 'development',
partialBundledLanguages: true,
interpolation: {
escapeValue: false,
},
ns: [DEFAULT_NS],
defaultNS: DEFAULT_NS,
fallbackNS: DEFAULT_NS,
load: DEFAULT_LOAD,
react: {
useSuspense: false,
},
resources: {},
parseMissingKeyHandler: (key, defaultValue) => defaultValue ?? startCase(key),
get initImmediate(): boolean {
return typeof window !== 'undefined'
},
}
================================================
FILE: src/locale/index.ts
================================================
import type { LocaleTranslations } from './config.js'
import deLocale from './de/translation.json' with { type: 'json' }
import enLocale from './en/translation.json' with { type: 'json' }
import esLocale from './es/translation.json' with { type: 'json' }
import itLocale from './it/translation.json' with { type: 'json' }
import jaLocale from './ja/translation.json' with { type: 'json' }
import plLocale from './pl/translation.json' with { type: 'json' }
import ptBrLocale from './pt-BR/translation.json' with { type: 'json' }
import uaLocale from './ua/translation.json' with { type: 'json' }
import zhCNLocale from './zh-CN/translation.json' with { type: 'json' }
export * from './config.js'
export * from './default-config.js'
export const locales: Record<string, LocaleTranslations> = {
de: deLocale,
en: enLocale,
es: esLocale,
it: itLocale,
ja: jaLocale,
pl: plLocale,
'pt-BR': ptBrLocale,
ua: uaLocale,
'zh-CN': zhCNLocale,
}
================================================
FILE: src/utils/error-type.enum.ts
================================================
// eslint-disable-next-line no-shadow
export enum ErrorTypeEnum {
App = 'AppError',
Configuration = 'ConfigurationError',
Forbidden = 'ForbiddenError',
NotFound = 'NotFoundError',
NotImplemented = 'NotImplementedError',
Record = 'RecordError',
Validation = 'ValidationError',
}
export default ErrorTypeEnum
================================================
FILE: src/utils/index.ts
================================================
export * from './error-type.enum.js'
export * from './translate-functions.factory.js'
export * from './flat/index.js'
export * from './param-converter/index.js'
================================================
FILE: src/utils/theme-bundler.ts
================================================
import { createRequire } from 'node:module'
import path from 'path'
const require = createRequire(import.meta.url)
const getAdminjsThemesDir = () => path.parse(require.resolve('@adminjs/themes')).dir
export const bundlePath = (theme: string): string => path.join(getAdminjsThemesDir(), `themes/${theme}/theme.bundle.js`)
export const stylePath = (theme: string): string => path.join(getAdminjsThemesDir(), `themes/${theme}/style.css`)
================================================
FILE: src/utils/flat/constants.ts
================================================
const DELIMITER = '.'
export { DELIMITER }
================================================
FILE: src/utils/flat/filter-out-params.doc.md
================================================
From all keys in `params` it removes this passed in an argument.
### Example
```javascript
import flat from '@adminjs'
const params = {
name: 'John',
'education.school.name': 'Harvard',
'education.school.id': 123,
}
flat.filterOutParams(params, 'education.school')
// results to {
// name: 'John',
// }
flat.filterOurParams(params, 'name')
// results to {
// 'education.school.name': 'Harvard',
// 'education.school.id': 123,
// }
================================================
FILE: src/utils/flat/filter-out-params.ts
================================================
import { propertyKeyRegex } from './property-key-regex.js'
import { FlattenParams } from './flat.types.js'
/**
* @load ./filter-out-params.doc.md
* @memberof module:flat
* @param {FlattenParams} params
* @param {string | Array<string>} properties
* @returns {FlattenParams}
*/
const filterOutParams = (
params: FlattenParams,
properties: string | Array<string>,
): FlattenParams => {
const propertyArray = Array.isArray(properties) ? properties : [properties]
return propertyArray
.filter((propertyPath) => !!propertyPath)
.reduce((globalFiltered, propertyPath) => {
const regex = propertyKeyRegex(propertyPath)
return Object.keys(globalFiltered)
.filter((key) => !key.match(regex))
.reduce((memo, key) => {
memo[key] = (params[key] as string)
return memo
}, {} as FlattenParams)
}, params)
}
export { filterOutParams }
================================================
FILE: src/utils/flat/flat.types.ts
================================================
/**
* Type of flatten params.
*
* @memberof module:flat
* @alias FlattenParams
*/
export type FlattenParams = {
[key: string]: FlattenValue;
}
export type FlattenValue = string
| boolean
| number
| Date
| null
| []
| Record<string, unknown>
| File
/**
* @memberof module:flat
* @alias GetOptions
*/
export type GetOptions = {
/**
* Indicates if all the "less related" siblings should be included. This option takes care of
* fetching elements in nested arrays. Let's say you have keys: `nested.0.array.0` and `
* `nested.1.array.0.`. With `includeAllSiblings` you will fetch all nested.N.array elements.
*/
includeAllSiblings?: boolean;
}
/**
* Available types for flatten values. This is an Union of types:
* - `string`
* - `boolean`
* - `number`
* - `Date`
* - `null`
* - `[]` (empty array)
* - `{}` (empty object)
* - `File`
* @memberof module:flat
* @alias FlattenValue
* @typedef {Union} FlattenValue
*/
================================================
FILE: src/utils/flat/get.doc.md
================================================
Returns sub-property from the flatten params. When the property path is not given function returns
an entire unflatten object.
### Example
```javascript
import flat from '@adminjs'
const params = {
name: 'John',
'education.school.name': 'Harvard',
'education.school.id': 123,
}
const data = flat.get(params, 'education.school')
// results to {
// name: 'Harvard',
// id: 321,
// }
// value is undefined
const data = flat.get(params)
// results to {
// name: 'John',
// education: {
// school: {
// name: 'Harvard',
// id: 321,
// }
// }
// }
```
================================================
FILE: src/utils/flat/index.ts
================================================
export * from './flat-module.js'
export * from './flat.types.js'
================================================
FILE: src/utils/flat/merge.ts
================================================
import flat from 'flat'
import { FlattenParams } from './flat.types.js'
import { set } from './set.js'
/**
* Merges params together and returns flatten result
*
* @param {any} params
* @param {Array<any>} ...mergeParams
* @returns {FlattenParams}
* @memberof module:flat
*/
const merge = (params: any = {}, ...mergeParams: Array<any>): FlattenParams => {
const flattenParams = flat.flatten(params)
// reverse because we merge from right
return mergeParams.reverse().reduce((globalMemo, mergeParam) => (
Object.keys(mergeParam)
.reduce((memo, key) => (set(memo, key, mergeParam[key])), globalMemo)
), flattenParams as Record<string, any>)
}
export { merge }
================================================
FILE: src/utils/flat/path-parts.type.ts
================================================
export type PathParts = Array<string>
================================================
FILE: src/utils/flat/path-to-parts.doc.md
================================================
the Long story short this method:
- changes: `nested.nested2.normalInner`
- to `["nested", "nested.nested2", "nested.nested2.normalInner"]`
So it can be used to search for the param in a {@link FlattenParams} object.
Formally it changes path in "flatten" notation, to an Array of all possible
keys, which could have searched property.
When `skipArrayIndexes` is set to true it also it takes care of the arrays, which are
separated by numbers (indexes). Then it:
- changes: `nested.0.normalInner.1`
- to: `nested.normalInner`
Everything because when we look for a property of a given path it can be inside a
mixed property. So first, we have to find top-level mixed property, and then,
step by step, find inside each of them.
================================================
FILE: src/utils/flat/path-to-parts.ts
================================================
import { PathParts } from './path-parts.type.js'
/**
* @memberof module:flat
* @alias PathToPartsOptions
*/
export type PathToPartsOptions = {
/**
* Indicates if array indexes should be skipped from the outcome.
*/
skipArrayIndexes?: boolean;
}
/**
* @load ./path-to-parts.doc.md
* @param {string} propertyPath
* @param {PathToPartsOptions} options
* @returns {PathParts}
*
* @memberof module:flat
* @alias pathToParts
*/
const pathToParts = (propertyPath: string, options: PathToPartsOptions = {}): PathParts => {
let allParts = propertyPath.split('.')
if (options.skipArrayIndexes) {
// eslint-disable-next-line no-restricted-globals
allParts = allParts.filter((part) => isNaN(+part))
}
return allParts.reduce((memo, part) => {
if (memo.length) {
return [
...memo,
[memo[memo.length - 1], part].join('.'),
]
}
return [part]
}, [] as Array<string>)
}
export { pathToParts }
================================================
FILE: src/utils/flat/property-key-regex.ts
================================================
import { DELIMITER } from './constants.js'
import { GetOptions } from './flat.types.js'
// this is the regex used to find all existing properties starting with a key
export const propertyKeyRegex = (propertyPath: string, options?: GetOptions): RegExp => {
const delimiter = new RegExp(`\\${DELIMITER}`, 'g')
const escapedDelimiter = `\\${DELIMITER}`
// but for `nested.1.property.0` it will produce `nested(\.|\.\d+\.)1(\.|\.\d+\.)property.0`
// and this is intentional because user can give an one index in property path for with deeply
// nested arrays
const escapedDelimiterOrIndex = `(${escapedDelimiter}|${escapedDelimiter}\\d+${escapedDelimiter})`
const path = options?.includeAllSiblings
? propertyPath.replace(delimiter, escapedDelimiterOrIndex)
: propertyPath.replace(delimiter, escapedDelimiter)
return new RegExp(`^${path}($|${escapedDelimiter})`, '')
}
================================================
FILE: src/utils/flat/select-params.doc.md
================================================
From all keys in `params` it selects only those passed in arguments.
### Example
```javascript
import flat from '@adminjs'
const params = {
name: 'John',
'education.school.name': 'Harvard',
'education.school.id': 123,
}
flat.selectParams(params, 'education.school')
// results to {
// 'education.school.name': 'Harvard',
// 'education.school.id': 123,
// }
flat.selectParams(params, 'education.school.id', 'name')
// results to {
// 'name': 'John',
// 'education.school.id': 123,
// }
================================================
FILE: src/utils/flat/set.doc.md
================================================
Updates the flatten param object with a given value. Value can be anything and, this anything will
be flattened and added to `params`.
`params` is not mutated here.
### Example
```javascript
import flat from '@adminjs'
const params = {
name: 'John',
'education.school.name': 'Harvard',
'education.school.id': 123,
}
const data = flat.set(params, 'education.shool', {
name: 'Yale',
id: 321,
})
// results to data === {
// name: 'John',
// 'education.school.name': 'Yale`,
// 'education.school.id': 321,
// }
// value is undefined
const data = flat.set(params, 'education') // results to data === { name: 'John' }
```
================================================
FILE: src/utils/param-converter/constants.ts
================================================
const DELIMITER = '.'
export { DELIMITER }
================================================
FILE: src/utils/param-converter/convert-nested-param.ts
================================================
import { BasePropertyJSON } from '../../frontend/interfaces/property-json/property-json.interface.js'
import { DELIMITER } from './constants.js'
import { convertParam } from './convert-param.js'
const convertNestedParam = (
parentValue: Record<string, any>,
subProperty: BasePropertyJSON,
): Record<string, any> => {
const path = subProperty.propertyPath.split(DELIMITER).slice(-1)[0]
const { type = 'string' } = subProperty
let value = parentValue[path]
if (type === 'mixed' && value) {
const nestedSubProperties = subProperty.subProperties
for (const nestedSubProperty of nestedSubProperties) {
if (subProperty.isArray) {
value = [...value].map((element) => convertNestedParam(element, nestedSubProperty))
} else {
value = convertNestedParam(value, nestedSubProperty)
}
}
} else {
value = convertParam(value, subProperty.type)
}
return {
...parentValue,
[path]: value,
}
}
export { convertNestedParam }
================================================
FILE: src/utils/param-converter/convert-param.spec.ts
================================================
import { expect } from 'chai'
import { convertParam } from './convert-param.js'
describe('module:paramConverter.convertParam', () => {
it('should convert numeric strings to Number', () => {
expect(convertParam('123', 'number')).to.equal(123)
})
it('should convert bool strings to Boolean', () => {
/*
This will actually evaluate any string with length > 0 to true
Ideally, additional validation should be added to convertParam
*/
expect(convertParam('true', 'boolean')).to.equal(true)
})
it('should convert datetime strings to Date', () => {
expect(convertParam('2021-11-08', 'datetime').getTime()).to.equal(new Date('2021-11-08').getTime())
})
it('should leave other values unchanged', () => {
expect(convertParam(null, 'some other type')).to.equal(null)
})
})
================================================
FILE: src/utils/param-converter/convert-param.ts
================================================
const convertParam = (value: any, propertyType: string): any => {
if (value === null || typeof value === 'undefined') {
return value
}
if (propertyType === 'number') {
return Number(value)
}
if (propertyType === 'boolean') {
return Boolean(value)
}
if (['datetime', 'date'].includes(propertyType)) {
return new Date(value)
}
return value
}
export { convertParam }
================================================
FILE: src/utils/param-converter/index.ts
================================================
export * from './param-converter-module.js'
================================================
FILE: src/utils/param-converter/param-converter-module.ts
================================================
import { DELIMITER } from './constants.js'
import { convertNestedParam } from './convert-nested-param.js'
import { convertParam } from './convert-param.js'
import { prepareParams } from './prepare-params.js'
export type ParamConverterModuleType = {
convertParam: typeof convertParam;
convertNestedParam: typeof convertNestedParam;
prepareParams: typeof prepareParams;
DELIMITER: typeof DELIMITER;
}
export const paramConverter: ParamConverterModuleType = {
convertParam,
convertNestedParam,
DELIMITER,
prepareParams,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment