Showing preview only (1,178K chars total). Download the full file or copy to clipboard to get everything.
Repository: SoftwareBrothers/adminjs
Branch: master
Commit: 9c2ee7f1b58c
Files: 494
Total size: 1.0 MB
Directory structure:
gitextract_d7cte0cf/
├── .babelrc.json
├── .cspell.json
├── .dockerignore
├── .eslintrc.cjs
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── feature_request.yml
│ └── workflows/
│ └── push.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .prettierrc
├── .releaserc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── UPGRADE-6.0.md
├── bin/
│ ├── app.js
│ └── globals.js
├── cli.js
├── commitlint.config.cjs
├── cy/
│ ├── commands/
│ │ ├── ab-get-property.js
│ │ ├── ab-keep-logged-in.js
│ │ ├── ab-login-api.js
│ │ └── ab-login.js
│ ├── cypress.doc.md
│ ├── index.d.ts
│ ├── index.js
│ └── readme.md
├── index.d.ts
├── index.js
├── package.json
├── project.inlang/
│ ├── project_id
│ └── settings.json
├── spec/
│ ├── backend/
│ │ ├── helpers/
│ │ │ ├── helper-stub.ts
│ │ │ └── resource-stub.ts
│ │ └── index.js
│ ├── fixtures/
│ │ ├── action.factory.js
│ │ ├── example-component.js
│ │ ├── property.factory.js
│ │ └── record.factory.js
│ ├── index.js
│ ├── lib.js
│ └── setup.js
├── src/
│ ├── adminjs-options.interface.ts
│ ├── adminjs.spec.ts
│ ├── adminjs.ts
│ ├── babel.test.config.json
│ ├── backend/
│ │ ├── actions/
│ │ │ ├── action.interface.ts
│ │ │ ├── bulk-delete/
│ │ │ │ ├── bulk-delete-action.spec.ts
│ │ │ │ └── bulk-delete-action.ts
│ │ │ ├── delete/
│ │ │ │ ├── delete-action.spec.ts
│ │ │ │ └── delete-action.ts
│ │ │ ├── edit/
│ │ │ │ └── edit-action.ts
│ │ │ ├── index.ts
│ │ │ ├── list/
│ │ │ │ └── list-action.ts
│ │ │ ├── new/
│ │ │ │ └── new-action.ts
│ │ │ ├── search/
│ │ │ │ └── search-action.ts
│ │ │ └── show/
│ │ │ └── show-action.ts
│ │ ├── adapters/
│ │ │ ├── database/
│ │ │ │ ├── base-database.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── property/
│ │ │ │ ├── base-property.ts
│ │ │ │ └── index.ts
│ │ │ ├── record/
│ │ │ │ ├── base-record.spec.ts
│ │ │ │ ├── base-record.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── params.type.ts
│ │ │ └── resource/
│ │ │ ├── base-resource.spec.ts
│ │ │ ├── base-resource.ts
│ │ │ ├── index.ts
│ │ │ └── supported-databases.type.ts
│ │ ├── bundler/
│ │ │ ├── app.bundler.ts
│ │ │ ├── components.bundler.ts
│ │ │ ├── generate-user-component-entry.spec.js
│ │ │ ├── generate-user-component-entry.ts
│ │ │ ├── globals.bundler.ts
│ │ │ ├── index.ts
│ │ │ └── utils/
│ │ │ ├── asset-bundler.ts
│ │ │ └── constants.ts
│ │ ├── controllers/
│ │ │ ├── api-controller.spec.js
│ │ │ ├── api-controller.ts
│ │ │ ├── app-controller.ts
│ │ │ └── index.ts
│ │ ├── decorators/
│ │ │ ├── action/
│ │ │ │ ├── action-decorator.spec.ts
│ │ │ │ ├── action-decorator.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── property/
│ │ │ │ ├── index.ts
│ │ │ │ ├── property-decorator.spec.ts
│ │ │ │ ├── property-decorator.ts
│ │ │ │ ├── property-options.interface.ts
│ │ │ │ └── utils/
│ │ │ │ ├── index.ts
│ │ │ │ ├── override-from-options.spec.ts
│ │ │ │ └── override-from-options.ts
│ │ │ └── resource/
│ │ │ ├── index.ts
│ │ │ ├── resource-decorator.spec.ts
│ │ │ ├── resource-decorator.ts
│ │ │ ├── resource-options.interface.ts
│ │ │ └── utils/
│ │ │ ├── decorate-actions.ts
│ │ │ ├── decorate-properties.spec.ts
│ │ │ ├── decorate-properties.ts
│ │ │ ├── find-sub-property.ts
│ │ │ ├── flat-sub-properties.ts
│ │ │ ├── get-navigation.spec.ts
│ │ │ ├── get-navigation.ts
│ │ │ ├── get-property-by-key.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── services/
│ │ │ ├── action-error-handler/
│ │ │ │ ├── action-error-handler.spec.ts
│ │ │ │ ├── action-error-handler.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ └── sort-setter/
│ │ │ ├── index.ts
│ │ │ ├── sort-setter.spec.js
│ │ │ └── sort-setter.ts
│ │ └── utils/
│ │ ├── auth/
│ │ │ ├── base-auth-provider.ts
│ │ │ ├── default-auth-provider.ts
│ │ │ └── index.ts
│ │ ├── build-feature/
│ │ │ ├── build-feature.spec.ts
│ │ │ ├── build-feature.ts
│ │ │ └── index.ts
│ │ ├── component-loader.ts
│ │ ├── errors/
│ │ │ ├── app-error.ts
│ │ │ ├── configuration-error.ts
│ │ │ ├── forbidden-error.ts
│ │ │ ├── index.ts
│ │ │ ├── not-found-error.ts
│ │ │ ├── not-implemented-error.ts
│ │ │ ├── record-error.ts
│ │ │ └── validation-error.ts
│ │ ├── filter/
│ │ │ ├── filter.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── layout-element-parser/
│ │ │ ├── index.ts
│ │ │ ├── layout-element-parser.spec.ts
│ │ │ ├── layout-element-parser.ts
│ │ │ └── layout-element.doc.md
│ │ ├── options-parser/
│ │ │ ├── index.ts
│ │ │ └── options-parser.ts
│ │ ├── populator/
│ │ │ ├── index.ts
│ │ │ ├── populate-property.spec.ts
│ │ │ ├── populate-property.ts
│ │ │ ├── populator.doc.md
│ │ │ ├── populator.spec.ts
│ │ │ └── populator.ts
│ │ ├── request-parser/
│ │ │ ├── index.ts
│ │ │ ├── request-parser.spec.ts
│ │ │ └── request-parser.ts
│ │ ├── resources-factory/
│ │ │ ├── index.ts
│ │ │ ├── resources-factory.spec.js
│ │ │ └── resources-factory.ts
│ │ ├── router/
│ │ │ ├── index.ts
│ │ │ ├── router.doc.md
│ │ │ ├── router.spec.ts
│ │ │ └── router.ts
│ │ ├── uploaded-file.type.ts
│ │ └── view-helpers/
│ │ ├── index.ts
│ │ ├── view-helpers.spec.ts
│ │ └── view-helpers.ts
│ ├── constants.ts
│ ├── core-scripts.interface.ts
│ ├── current-admin.interface.ts
│ ├── frontend/
│ │ ├── assets/
│ │ │ └── styles/
│ │ │ └── icomoon.css
│ │ ├── bundle-entry.jsx
│ │ ├── components/
│ │ │ ├── actions/
│ │ │ │ ├── action.props.ts
│ │ │ │ ├── bulk-delete.tsx
│ │ │ │ ├── edit.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── list.tsx
│ │ │ │ ├── new.tsx
│ │ │ │ ├── show.tsx
│ │ │ │ └── utils/
│ │ │ │ ├── append-force-refresh.spec.ts
│ │ │ │ ├── append-force-refresh.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout-element-renderer.tsx
│ │ │ ├── app/
│ │ │ │ ├── action-button/
│ │ │ │ │ ├── action-button.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── action-header/
│ │ │ │ │ ├── action-header-props.tsx
│ │ │ │ │ ├── action-header.tsx
│ │ │ │ │ ├── actions-to-button-group.spec.ts
│ │ │ │ │ ├── actions-to-button-group.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── styled-back-button.tsx
│ │ │ │ ├── admin-modal.tsx
│ │ │ │ ├── app-loader.tsx
│ │ │ │ ├── auth-background-component.tsx
│ │ │ │ ├── base-action-component.tsx
│ │ │ │ ├── breadcrumbs.tsx
│ │ │ │ ├── default-dashboard.tsx
│ │ │ │ ├── drawer-portal.tsx
│ │ │ │ ├── error-boundary.tsx
│ │ │ │ ├── error-message.tsx
│ │ │ │ ├── filter-drawer.tsx
│ │ │ │ ├── footer.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── language-select/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── language-select.tsx
│ │ │ │ ├── logged-in.tsx
│ │ │ │ ├── notice.tsx
│ │ │ │ ├── records-table/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── no-records.tsx
│ │ │ │ │ ├── property-header.spec.tsx
│ │ │ │ │ ├── property-header.tsx
│ │ │ │ │ ├── record-in-list.tsx
│ │ │ │ │ ├── records-table-header.spec.tsx
│ │ │ │ │ ├── records-table-header.tsx
│ │ │ │ │ ├── records-table.spec.tsx
│ │ │ │ │ ├── records-table.tsx
│ │ │ │ │ ├── selected-records.tsx
│ │ │ │ │ └── utils/
│ │ │ │ │ ├── display.tsx
│ │ │ │ │ ├── get-bulk-actions-from-records.spec.ts
│ │ │ │ │ └── get-bulk-actions-from-records.ts
│ │ │ │ ├── sidebar/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── sidebar-branding.tsx
│ │ │ │ │ ├── sidebar-footer.tsx
│ │ │ │ │ ├── sidebar-pages.tsx
│ │ │ │ │ ├── sidebar-resource-section.tsx
│ │ │ │ │ └── sidebar.tsx
│ │ │ │ ├── sort-link.tsx
│ │ │ │ ├── top-bar.tsx
│ │ │ │ ├── utils/
│ │ │ │ │ ├── discord-logo-svg.tsx
│ │ │ │ │ └── rocket-svg.tsx
│ │ │ │ └── version.tsx
│ │ │ ├── application.tsx
│ │ │ ├── index.ts
│ │ │ ├── login/
│ │ │ │ └── index.tsx
│ │ │ ├── property-type/
│ │ │ │ ├── array/
│ │ │ │ │ ├── add-new-item-translation.tsx
│ │ │ │ │ ├── convert-to-sub-property.tsx
│ │ │ │ │ ├── edit.spec.tsx
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── remove-sub-property.spec.ts
│ │ │ │ │ ├── remove-sub-property.ts
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── base-property-component.doc.md
│ │ │ │ ├── base-property-component.tsx
│ │ │ │ ├── base-property-props.ts
│ │ │ │ ├── boolean/
│ │ │ │ │ ├── boolean-property-value.tsx
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── map-value.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── clean-property-component.tsx
│ │ │ │ ├── currency/
│ │ │ │ │ ├── currency-input-wrapper.tsx
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── format-value.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── datetime/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── map-value.ts
│ │ │ │ │ ├── show.tsx
│ │ │ │ │ └── strip-time-from-iso.ts
│ │ │ │ ├── default-type/
│ │ │ │ │ ├── default-property-value.tsx
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── show.spec.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── docs/
│ │ │ │ │ └── on-property-change.doc.md
│ │ │ │ ├── index.tsx
│ │ │ │ ├── key-value/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── mixed/
│ │ │ │ │ ├── convert-to-sub-property.ts
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── password/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── phone/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── record-property-is-equal.ts
│ │ │ │ ├── reference/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── reference-value.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── richtext/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── textarea/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── show.tsx
│ │ │ │ └── utils/
│ │ │ │ ├── index.ts
│ │ │ │ ├── property-description/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── property-description.tsx
│ │ │ │ └── property-label/
│ │ │ │ ├── index.ts
│ │ │ │ └── property-label.tsx
│ │ │ ├── routes/
│ │ │ │ ├── bulk-action.tsx
│ │ │ │ ├── dashboard.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── page.tsx
│ │ │ │ ├── record-action.spec.tsx
│ │ │ │ ├── record-action.tsx
│ │ │ │ ├── resource-action.tsx
│ │ │ │ ├── resource.tsx
│ │ │ │ └── utils/
│ │ │ │ ├── should-action-re-fetch-data.ts
│ │ │ │ └── wrapper.tsx
│ │ │ └── spec/
│ │ │ ├── action-json.factory.ts
│ │ │ ├── factory.ts
│ │ │ ├── initialize-translations.ts
│ │ │ ├── page-json.factory.ts
│ │ │ ├── property-json.factory.ts
│ │ │ ├── record-json.factory.ts
│ │ │ ├── resource-json.factory.ts
│ │ │ └── test-context-provider.tsx
│ │ ├── global-entry.js
│ │ ├── hoc/
│ │ │ ├── allow-override.tsx
│ │ │ ├── index.ts
│ │ │ ├── with-no-ssr.tsx
│ │ │ └── with-notice.ts
│ │ ├── hooks/
│ │ │ ├── index.ts
│ │ │ ├── use-action/
│ │ │ │ ├── index.ts
│ │ │ │ ├── use-action-response-handler.ts
│ │ │ │ ├── use-action.doc.md
│ │ │ │ ├── use-action.ts
│ │ │ │ └── use-action.types.ts
│ │ │ ├── use-current-admin.ts
│ │ │ ├── use-filter-drawer.tsx
│ │ │ ├── use-history-listen.ts
│ │ │ ├── use-local-storage/
│ │ │ │ ├── index.ts
│ │ │ │ ├── use-local-storage-result.type.ts
│ │ │ │ ├── use-local-storage.doc.md
│ │ │ │ └── use-local-storage.ts
│ │ │ ├── use-modal.doc.md
│ │ │ ├── use-modal.ts
│ │ │ ├── use-navigation-resources.ts
│ │ │ ├── use-notice.ts
│ │ │ ├── use-query-params.ts
│ │ │ ├── use-record/
│ │ │ │ ├── filter-record.spec.ts
│ │ │ │ ├── filter-record.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── is-entire-record-given.ts
│ │ │ │ ├── merge-record-response.ts
│ │ │ │ ├── params-to-form-data.spec.ts
│ │ │ │ ├── params-to-form-data.ts
│ │ │ │ ├── update-record.spec.ts
│ │ │ │ ├── update-record.ts
│ │ │ │ ├── use-record.doc.md
│ │ │ │ ├── use-record.tsx
│ │ │ │ └── use-record.type.ts
│ │ │ ├── use-records/
│ │ │ │ ├── index.ts
│ │ │ │ ├── use-records-result.type.ts
│ │ │ │ ├── use-records.doc.md
│ │ │ │ └── use-records.ts
│ │ │ ├── use-resource/
│ │ │ │ ├── index.ts
│ │ │ │ ├── use-resource.doc.md
│ │ │ │ └── use-resource.ts
│ │ │ ├── use-selected-records/
│ │ │ │ ├── index.ts
│ │ │ │ ├── use-selected-records-result.type.ts
│ │ │ │ ├── use-selected-records.doc.md
│ │ │ │ └── use-selected-records.ts
│ │ │ └── use-translation.ts
│ │ ├── index.ts
│ │ ├── interfaces/
│ │ │ ├── action/
│ │ │ │ ├── action-has-component.ts
│ │ │ │ ├── action-href.ts
│ │ │ │ ├── action-json.interface.ts
│ │ │ │ ├── build-action-api-call-trigger.ts
│ │ │ │ ├── build-action-click-handler.ts
│ │ │ │ ├── build-action-test-id.ts
│ │ │ │ ├── call-action-api.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── is-bulk-action.ts
│ │ │ │ ├── is-record-action.ts
│ │ │ │ └── is-resource-action.ts
│ │ │ ├── index.ts
│ │ │ ├── modal.interface.ts
│ │ │ ├── noticeMessage.interface.ts
│ │ │ ├── page-json.interface.ts
│ │ │ ├── property-json/
│ │ │ │ ├── index.ts
│ │ │ │ └── property-json.interface.ts
│ │ │ ├── record-json.interface.ts
│ │ │ └── resource-json.interface.ts
│ │ ├── layout-template.spec.ts
│ │ ├── layout-template.tsx
│ │ ├── login-template.spec.ts
│ │ ├── login-template.tsx
│ │ ├── store/
│ │ │ ├── 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
│ │ │ ├── index.ts
│ │ │ ├── initialize-store.ts
│ │ │ ├── reducers/
│ │ │ │ ├── assetsReducer.ts
│ │ │ │ ├── brandingReducer.ts
│ │ │ │ ├── dashboardReducer.ts
│ │ │ │ ├── drawerReducer.ts
│ │ │ │ ├── filterDrawerReducer.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── localesReducer.ts
│ │ │ │ ├── modalReducer.ts
│ │ │ │ ├── noticesReducer.ts
│ │ │ │ ├── pagesReducer.ts
│ │ │ │ ├── pathsReducer.ts
│ │ │ │ ├── resourcesReducer.ts
│ │ │ │ ├── routerReducer.ts
│ │ │ │ ├── sessionReducer.ts
│ │ │ │ ├── themeReducer.ts
│ │ │ │ └── versionsReducer.ts
│ │ │ ├── store.ts
│ │ │ └── utils/
│ │ │ └── pages-to-store.ts
│ │ └── utils/
│ │ ├── adminjs.i18n.ts
│ │ ├── api-client.ts
│ │ ├── data-css-name.ts
│ │ ├── index.ts
│ │ └── overridable-component.ts
│ ├── index.ts
│ ├── locale/
│ │ ├── config.ts
│ │ ├── de/
│ │ │ └── translation.json
│ │ ├── default-config.ts
│ │ ├── en/
│ │ │ └── translation.json
│ │ ├── es/
│ │ │ └── translation.json
│ │ ├── index.ts
│ │ ├── it/
│ │ │ └── translation.json
│ │ ├── ja/
│ │ │ └── translation.json
│ │ ├── pl/
│ │ │ └── translation.json
│ │ ├── pt-BR/
│ │ │ └── translation.json
│ │ ├── ua/
│ │ │ └── translation.json
│ │ └── zh-CN/
│ │ └── translation.json
│ └── utils/
│ ├── error-type.enum.ts
│ ├── file-resolver.ts
│ ├── flat/
│ │ ├── constants.ts
│ │ ├── filter-out-params.doc.md
│ │ ├── filter-out-params.spec.ts
│ │ ├── filter-out-params.ts
│ │ ├── flat-module.ts
│ │ ├── flat.doc.md
│ │ ├── flat.types.ts
│ │ ├── get.doc.md
│ │ ├── get.spec.ts
│ │ ├── get.ts
│ │ ├── index.ts
│ │ ├── merge.spec.ts
│ │ ├── merge.ts
│ │ ├── path-parts.type.ts
│ │ ├── path-to-parts.doc.md
│ │ ├── path-to-parts.ts
│ │ ├── property-key-regex.ts
│ │ ├── remove-path.doc.md
│ │ ├── remove-path.spec.ts
│ │ ├── remove-path.ts
│ │ ├── select-params.doc.md
│ │ ├── select-params.spec.ts
│ │ ├── select-params.ts
│ │ ├── set.doc.md
│ │ ├── set.spec.ts
│ │ └── set.ts
│ ├── index.ts
│ ├── param-converter/
│ │ ├── constants.ts
│ │ ├── convert-nested-param.spec.ts
│ │ ├── convert-nested-param.ts
│ │ ├── convert-param.spec.ts
│ │ ├── convert-param.ts
│ │ ├── index.ts
│ │ ├── param-converter-module.ts
│ │ ├── prepare-params.ts
│ │ └── validate-param.ts
│ ├── theme-bundler.ts
│ └── translate-functions.factory.ts
├── tsconfig.json
└── vendor-types/
├── chai/
│ └── index.d.ts
├── global.ts
└── node/
└── node.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc.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"
],
"generatorOpts": {
"importAttributesKeyword": "with"
}
}
================================================
FILE: .cspell.json
================================================
{
"version": "0.1",
"language": "en",
"words": [
"crossorigin", "nullish", "envs", "prefetch", "expressjs", "icomoon", "populator",
"unflatten", "datetime", "richtext", "promisify", "hoverable", "dropdown", "flatpickr",
"codecov", "bulma", "unmount", "testid", "woff", "iife", "sourcemap", "Roboto",
"camelize", "datepicker", "camelcase", "fullwidth", "wysiwig", "Helvetica", "Neue",
"Arial", "nowrap", "textfield", "scrollable", "flexbox", "treal", "xxxl",
"adminjs", "Checkmark", "overridable", "Postgres", "Hana", "Wojtek", "Krysiak", "bigint",
"borderless", "metadetaksamosone", "esrever", "jsnext", "Никита"
],
"ignorePaths": [
"src/frontend/assets/**/*"
]
}
================================================
FILE: .dockerignore
================================================
node_modules
npm-debug.log
================================================
FILE: .eslintrc.cjs
================================================
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'mocha'],
env: {
es6: true,
node: true,
mocha: true,
},
extends: ['airbnb', 'plugin:@typescript-eslint/recommended', 'plugin:mocha/recommended', 'plugin:import/typescript'],
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
rules: {
'react/jsx-filename-extension': 'off',
'@typescript-eslint/no-explicit-any': 'off',
indent: ['error', 2],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'import/prefer-default-export': 'off',
'linebreak-style': ['error', 'unix'],
quotes: ['error', 'single'],
semi: ['error', 'never'],
'import/no-unresolved': 'off',
'func-names': 'off',
'no-underscore-dangle': 'off',
'guard-for-in': 'off',
'no-restricted-syntax': 'off',
'no-await-in-loop': 'off',
'object-curly-newline': 'off',
'import/extensions': [2, 'ignorePackages'],
'mocha/no-hooks-for-single-case': 'off',
'no-param-reassign': 'off',
'default-param-last': 'off',
'no-use-before-define': 'off',
'no-restricted-exports': 'off',
'react/require-default-props': 'off',
'react/jsx-props-no-spreading': 'off',
'react/function-component-definition': 'off',
'max-classes-per-file': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'import/no-import-module-exports': 'off',
},
ignorePatterns: [
'*/build/**/*',
'*.json',
'*.txt',
'*.md',
'*.lock',
'*.log',
'*.yaml',
'**/*/frontend/assets/**/*',
'*.d.ts',
'*.config.js',
],
overrides: [
{
files: ['*-test.js', '*.spec.js', '*-test.ts', '*.spec.ts', '*.spec.tsx', '*.factory.ts', '*.factory.js'],
rules: {
'no-unused-expressions': 'off',
'func-names': 'off',
'prefer-arrow-callback': 'off',
'import/no-extraneous-dependencies': 'off',
'mocha/no-mocha-arrows': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
},
},
{
files: ['*.jsx', '*.js'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
},
},
{
files: ['*.cjs'],
rules: {
'import/no-commonjs': 'off',
},
},
{
files: ['*.tsx'],
rules: {
'react/prop-types': 'off',
'react/function-component-definition': 'off',
},
},
{
files: ['**/*/cypress/integration/**/*.spec.js', './cy/**/*.js'],
rules: {
'mocha/no-mocha-arrows': 'off',
'spaced-comment': 'off',
},
},
],
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
globals: {
expect: true,
factory: true,
sandbox: true,
server: true,
window: true,
AdminJS: true,
flatpickr: true,
FormData: true,
File: true,
cy: true,
Cypress: true,
},
}
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug", "triage"]
assignees:
- octocat
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: Please visit our Discord channel [Discord](https://adminjs.page.link/discord) or leave your email.
validations:
required: false
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "List what you are trying to do?"
validations:
required: true
- type: input
id: prevalence
attributes:
label: Bug prevalence
description: "How often do you or others encounter this bug?"
placeholder: "Example: Whenever I visit the personal account page (1-2 times a week)"
validations:
required: true
- type: textarea
id: versions
attributes:
label: AdminJS dependencies version
description: "Provide to us a list of dependencies from package.json "
placeholder: "Copy exact versions of plugins that you are currently using"
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers do you see the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output or drag&drop screenshot of code. This will be automatically formatted into code, so no need for backticks.
render: bash
- type: textarea
id: code
attributes:
label: Relevant code that's giving you issues
description: Please copy and paste any relevant code or drag&drop screenshot of code. This will be automatically formatted into code, so no need for backticks.
render: TypeScript
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
---
name: 🛠️ Feature Request
description: Suggest an idea to help us improve AdminJS
title: "[Feature]: "
labels:
- "feature_request"
body:
- type: markdown
attributes:
value: |
**Thanks :heart: for taking the time to fill out this feature request report!**
We kindly ask that you search to see if an issue [already exists](https://github.com/SoftwareBrothers/adminjs#:~:text=OpenSource%20SoftwareBrothers%20community-,Join%20the%20community,-to%20get%20help) for your feature.
We are also happy to accept contributions from our users. For more details see [here](https://github.com/SoftwareBrothers/adminjs/blob/master/CONTRIBUTING.md).
- type: textarea
attributes:
label: Description
description: |
A clear and concise description of the feature you're interested in.
validations:
required: true
- type: textarea
attributes:
label: Suggested Solution
description: |
Describe the solution you'd like. A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Alternatives
description: |
Describe alternatives you've considered.
A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
attributes:
label: Additional Context
description: |
Add any other context about the problem here.
validations:
required: false
================================================
FILE: .github/workflows/push.yml
================================================
name: CI/CD
on: [push, pull_request]
jobs:
setup:
name: setup
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install
run: yarn install --frozen-lockfile
build:
name: build
runs-on: ubuntu-latest
needs: setup
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install
run: yarn install
- name: Assets cache
uses: actions/cache@v3
id: assets-cache
with:
path: src/frontend/assets/scripts
key: assets-${{ hashFiles('**/src/frontend/global-entry.js') }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/bin/bundle-globals.js') }}
restore-keys: |
assets-
- name: build
run: yarn build
- name: types
run: yarn types
- name: bundle globals production
if: steps.assets-cache.outputs.cache-hit != 'true'
run: NODE_ENV=production yarn bundle:globals
- name: bundle globals dev
if: steps.assets-cache.outputs.cache-hit != 'true'
run: NODE_ENV=dev yarn bundle:globals
- name: bundle production
run: NODE_ENV=production ONCE=true yarn bundle
- name: bundle dev
run: ONCE=true yarn bundle
- name: Upload Build
if: |
contains(github.ref, 'refs/heads/master')
|| contains(github.ref, 'refs/heads/beta-v7')
|| contains(github.ref, 'refs/heads/beta')
uses: actions/upload-artifact@v4
with:
name: lib
path: lib
- name: Upload Types
if: |
contains(github.ref, 'refs/heads/master')
|| contains(github.ref, 'refs/heads/beta-v7')
|| contains(github.ref, 'refs/heads/beta')
uses: actions/upload-artifact@v4
with:
name: types
path: types
- name: Upload Bundle
if: |
contains(github.ref, 'refs/heads/master')
|| contains(github.ref, 'refs/heads/beta-v7')
|| contains(github.ref, 'refs/heads/beta')
uses: actions/upload-artifact@v4
with:
name: bundle
path: lib/frontend/assets/scripts
test:
name: Test
runs-on: ubuntu-latest
needs: setup
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install
run: yarn install
- name: Lint
run: yarn lint
- name: spell
run: yarn cspell
- name: install codecov
run: yarn global add codecov
if: contains(github.ref, 'refs/heads/master')
- name: cover
if: contains(github.ref, 'refs/heads/master')
run: yarn codecov
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: test
if: "!contains(github.ref, 'refs/heads/master')"
run: yarn test
publish:
name: Publish
if: |
github.event_name == 'push'
&& (
contains(github.ref, 'refs/heads/master')
|| contains(github.ref, 'refs/heads/beta-v7')
|| contains(github.ref, 'refs/heads/beta')
)
needs:
- test
- build
services:
mongo:
image: mongo:3.4.23
ports:
- 27017:27017
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install
run: yarn install
- name: Download Build
uses: actions/download-artifact@v4
with:
name: lib
path: lib
- name: Download Types
uses: actions/download-artifact@v4
with:
name: types
path: types
- name: Download Bundle
uses: actions/download-artifact@v4
with:
name: bundle
path: bundle
- name: Check directories exists
uses: andstor/file-existence-action@v2
with:
files: "bundle/, lib/, types/"
fail: true
- name: Release
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }}
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
run: yarn release
================================================
FILE: .gitignore
================================================
.nyc_output
.DS_store
.vscode
.nova
.idea
.cache
lib
built
types
coverage
docker-compose.ovverride.yml
node_modules
src/frontend/assets/scripts
.adminjs
# cypress output
**/*/cypress/videos
**/*/cypress/screenshots
/packages/app/build
.env
firebase.env.json
yarn-error.log
================================================
FILE: .npmignore
================================================
.nyc_output
.DS_store
.vscode
.idea
.travis.yml
/coverage
docker-compose.ovverride.yml
node_modules
docs
/packages
================================================
FILE: .npmrc
================================================
access=public
================================================
FILE: .prettierrc
================================================
{
"printWidth": 100,
"singleQuote": true,
"trailingComma": "all",
"semi": false,
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"options": {
"parser": "typescript"
}
},
{
"files": "*.css",
"options": {
"singleQuote": false,
"tabWidth": 2
}
}
]
}
================================================
FILE: .releaserc
================================================
{
"branches": [
"+([0-9])?(.{+([0-9]),x}).x",
"master",
{
"name": "beta",
"prerelease": true
},
{
"name": "beta-v7",
"prerelease": true
}
],
"plugins": [
["@semantic-release/commit-analyzer", {
"preset": "angular",
"releaseRules": [
{"type": "feat", "scope": "locale", "release": "patch"},
{"type": "feat", "scope": "small", "release": "patch"},
{"type": "chore", "scope": "deps", "release": "patch"},
{"scope": "no-release", "release": false}
]
}],
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/github",
"@semantic-release/git"
]
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Attempting collaboration before conflict
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- Violence, threats of violence, or inciting others to commit self-harm
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, intentionally spreading misinformation, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Abuse of the reporting process to intentionally harass or exclude others
- Advocating for, or encouraging, any of the above behavior
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting us through adminjs@adminjs.co. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
If you are unsure whether an incident is a violation, or whether the space where the incident took place is covered by our Code of Conduct, **we encourage you to still report it**. We would prefer to have a few extra reports where we decide to take no action, than to leave an incident go unnoticed and unresolved that may result in an individual or group to feel like they can no longer participate in the community. Reports deemed as not a violation will also allow us to improve our Code of Conduct and processes surrounding it. If you witness a dangerous situation or someone in distress, we encourage you to report even if you are only an observer.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
================================================
FILE: CONTRIBUTING.md
================================================
# AdminJS Development
## Explanation of AdminJS libraries
There are three main terms we use to define AdminJS libraries, which are:
* Core - the main library, containing basically all the logic which you can find in `adminjs` repository
* Plugin - plugins are wrappers around popular frameworks which allow you to connect AdminJS to your Node.js server and build AdminJS-specific routes. For example `@adminjs/express`.
* Adapter - adapters are wrappers around supported ORMs and ODMs. They export classes which extend `BaseDatabase`, `BaseResource` and `BaseProperty` components. Their task is to translate AdminJS specific filters/queries to something understandable by your ORM/ODM of choice in order to communicate with the database. For example `@adminjs/typeorm`.
AdminJS also has got it's own design system library (`@adminjs/design-system`) and a set of premade `features` which you can import into your project (e. g. `@adminjs/passwords`).
## Contributing
### Development
If you want to contribute to the project, fork repositories you will be working on and after you're done with your changes, open a pull request.
AdminJS team uses [semantic-release](https://github.com/semantic-release/semantic-release) library to automatically create `releases` when a pull request is merged to `master` branch or `pre-releases` when a pull request is merged to `beta` branch. Additionally, based on your commit messages, the library automatically generates a changelog.
Because of this, there is a strict requirement on how you should name your `branches` and `commits`.
Branch names should be prefixed with `fix/`, `chore/`, `feat/` or `test/`.
Commit messages should start with `fix:`, `chore:`, `feat:`, `test:` with the following rules:
1. When a commit starting with `chore:` or `test:` is merged, it **will not** create a new release.
2. When a commit starting with `fix:` is merged, a release will be created which upgrades the patch version of the library (example: 6.0.0 -> 6.0.1).
3. When a commit starting with `feat:` is merged, a release will be created which upgrades the minor version of the library (example: 6.0.1 -> 6.1.0).
4. When a commit has `BREAKING CHANGE: xxxxx` inside of it's **description**, a release will be created which upgrades the major version of the library (example: 6.1.0 -> 7.0.0).
5. There are also additional rules defined in `.releaserc`:
- commits starting with `feat(locale):` and `feat(small):` will only upgrade the patch version despite being `feat` (feature) commits. This type of commit message should be used when you want to create a new translation (`feat(locale)`) or when you add a small feature that doesn't affect the library much (`feat(small)`).
- commits starting with `chore(deps):` will upgrade the patch version despite being `chore` commits. These are commits automatically created by [dependabot](https://github.com/dependabot) but you should also use this message type when you want to only upgrade a package manually.
6. When a pull request is merged which contains multiple commit messages, the higher version upgrades take precedence, for example a pull request with `feat:` and `fix:` messages will upgrade the `minor version` (because of `feat: ...` message) but both commit messages will appear in the changelog.
When you work on your bug fixes or new features, make sure they only concern the subject of your pull request. Ideally you would be using `git commit --amend` so that there's only one commit in your pull request.
If for some reason you want to make more than one change and you need to have multiple commit messages in a pull request, each commit message should only contain changes it refers to.
When a pull request contains a lot of commits without proper messages, we usually `squash` them when merging which can result in a poor release changelog.
### Issues
When creating an issue please try to describe your problem with as many details as possible. If your issue is a complex one and would require us to reproduce this to respond, a test repository or a code sample which would allow us to reproduce your problem would be ideal.
If possible, try to create issues by using our issues templates.
### Translations
You can contribute translations via the [fink localization editor](https://fink.inlang.com/github.com/SoftwareBrothers/adminjs).
[](https://fink.inlang.com/github.com/SoftwareBrothers/adminjs?ref=badge)
## Developing locally
### Basic example
To see your changes in your local environment you will, first of all, need a test application. You can clone our [example app](https://github.com/SoftwareBrothers/adminjs-example-app) or create your own.
Next, open terminal in your `adminjs` fork repository, install dependencies and run commands:
```bash
$ yarn dev
$ yarn bundle:globals # re-run it only after you install new dependencies
```
After this, run:
```bash
$ yarn link
```
to register `adminjs` under yarn's linked packages.
Now open terminal in your test application's project and link your local `adminjs` package:
```bash
$ yarn link "adminjs"
```
You should be able to see your local changes to `adminjs` package in your test application. However, you might have to restart your test application each time you make changes to `adminjs` local package.
### Advanced example
The case described above is the easiest one. Now let's assume you want to make changes to `@adminjs/design-system` package. This will require you to chain `yarn link` commands:
1. Fork and clone `adminjs-design-system` repository
2. Install dependencies of `adminjs-design-system` and run `yarn dev`
3. Register `@adminjs/design-system` under yarn's linked packages: `yarn link`
4. Since `@adminjs/design-system` is a dependency of `adminjs`, open `adminjs` project locally and run:
```bash
$ yarn link "@adminjs/design-system"
```
and rebuild it.
5. Run `yarn link` to register `adminjs` under yarn's linked packages (if you haven't done it before).
6. Link `adminjs` package in your test application:
```bash
$ yarn link "adminjs"
```
If you try to run your test application now, it should build fine, but you might see some errors when you try to open admin panel in your browser concerning multiple React instances.
This error occurs because when you install repository's dependencies locally, it will install all of them, meaning you will have `react` installed in your `adminjs-design-system` and `adminjs` folders but you may have only one React instance at a time. The easiest way to solve this would be to use a linked `react` package:
1. In your test application, run:
```bash
$ yarn add react@^18.1.0 react-dom@^18.1.0 styled-components@^5.3.5
```
We're also installing `react-dom` and `styled-components` because these two libraries also cause similar issues to `react`.
2. Navigate to installed `react`, `react-dom` and `styled-components` and register them under yarn linked packages:
```bash
$ cd node_modules/react && yarn link
$ cd ../../node_modules/react-dom && yarn link
$ cd ../../node_modules/react-i18next && yarn link
$ cd ../../node_modules/styled-components && yarn link
```
3. Now link those packages in your local `adminjs` and `adminjs-design-system` projects:
```bash
$ yarn link "react"
$ yarn link "react-dom"
$ yarn link "react-i18next"
$ yarn link "styled-components"
```
4. Your test application should now be working.
Having lots of registered packages can bring confusion, you can use the command below to find out which packages you have linked and where they are used:
```bash
$ ls -la ~/.config/yarn/link/
```
`yarn link` documentation: https://classic.yarnpkg.com/en/docs/cli/link
### Alternative to yarn link
The alternative to `yarn link` is to clone our [adminjs-dev](https://github.com/SoftwareBrothers/adminjs-dev) repository. This repository has all AdminJS packages added as submodules in one [yarn workspace](https://classic.yarnpkg.com/lang/en/docs/workspaces/). The repository's README should be detailed enough to get you started.
Please note that using `yarn workspaces` also has it's issues that we haven't yet resolved. One of them is that all submodules dependencies are installed in the top level (in the root directory of `adminjs-dev`) and installing or updating any new dependency in for example `packages/adminjs` won't update it's `yarn.lock` file and you have to do it manually.
The upside is that you don't have to deal with `yarn link` dependency issues plus you're able to see your local changes in other submodules in the same workspace immediately.
================================================
FILE: LICENSE.md
================================================
Copyright 2018 SoftwareBrothers.co
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# AdminJS
[AdminJS](https://adminjs.co/) is an automatic admin interface that can be plugged into your application. You, as a developer, provide database models (like posts, comments, stores, products or whatever else your application uses), and AdminJS generates UI which allows you (or other trusted users) to manage content.
Inspired by: [django admin](https://docs.djangoproject.com), [rails admin](https://github.com/sferik/rails_admin) and [active admin](https://activeadmin.info/).
## Example application
Check out our demo application:
- Login: `example@adminjs.co`
- Password: `password`
https://demo.adminjs.co
You can also have a look at our customized AdminJS dashboard which shows various library statistics:
https://stats.adminjs.co
## Getting started
- Check out the [documentation](https://docs.adminjs.co)
- Try the [live demo](https://demo.adminjs.co) as mentioned above
## Our open source community on Discord
- [Join the community](https://adminjs.page.link/discord) to get help and be inspired.
# What kind of problems it solves
So you have a working service built in Node.js. It uses (for example) [Hapi.js](https://hapijs.com/) for rendering a couple of REST routes and [mongoose](https://mongoosejs.com/) as the _connector_ to the database.
Everything works fine, but now you would like to:
* view all the data in the app,
* perform custom _business_ actions on objects in the database,
* bootstrap the tables with the _initial_ data,
* build custom report pages,
* allow other team members (not necessary programmers) to see what is going on in the application.
And all these cases can be solved by AdminJS. By adding couple of lines of code you have a running admin interface.
# Features
* CRUD any data in any resource
* Custom actions
* Form validation based on schema in your resources
* Full featured dashboard with widgets
* Custom resource decorators
## Contribute
If you would like work on an AdminJS and develop new features please check out our [Contribution Guide](https://github.com/SoftwareBrothers/adminjs/blob/master/CONTRIBUTING.md)
There you can find instructions on how to run AdminJS locally for development.
If you're searching for tasks you can contribute to, we currently accept contributions to issues in our [Kanban Board](https://github.com/orgs/SoftwareBrothers/projects/5/views/1).
Any small or large contribution or any input into discussion is welcome!
## License
AdminJS is copyrighted © 2023 rst.software. It is a free software, and may be redistributed under the terms specified in the [LICENSE](LICENSE.md) file.
## About rst.software
<img src="https://pbs.twimg.com/profile_images/1367119173604810752/dKVlj1YY_400x400.jpg" width=150>
We’re an open, friendly team that helps clients from all over the world to transform their businesses and create astonishing products.
* We are available for [hire](https://www.rst.software/estimate-your-project).
* If you want to work for us - check out the [career page](https://www.rst.software/join-us).
================================================
FILE: UPGRADE-6.0.md
================================================
# Migration guide to version v6
## Updating AdminJS to v6
To update AdminJS package to the sixth wersion please use following npm command
```
npm install adminjs
```
This should update the version of ```adminjs``` and ```adminjs-design-system``` packages to newest beta versions.
If you have ```adminjs-design-system``` in your dependencies you should update it accordingly.
## Changes
### :warning: React upgrade to v18.1.0+
**If you don't have react as a dependency in your project you won't have to do anything 😉**
AdminJS now uses ```react``` and ```react-dom``` in ```v18.1.0+``` as a dependency. Hence if you're using react outside of AdminJS, please upgrade it to the matching version.
Instructions on how to do it are available [here](https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html)
Additionally, we upgraded the ```styled-components``` package to ```v5.3.5```, which works well with react v18.
### :warning: Rebranding
Branding option `softwareBrothers` is now not supported and replaced with `withMadeWithLove`
which shows a tiny heart icon on the bottom sidebar and login page.
### ⚠️ New RichText input library
Due to security and support issues, richText implementation has changed from Quill to TipTap. Therefore, in AdminJS v6, **all quill-related configurations will no longer be valid.**
### :white_check_mark: Phone and currency inputs
AdminJS gained two new input types. To use phone and currency inputs, use them as a type in your resource option.
**Example of use**
```ts
const TransactionResource: ResourceWithOptions = {
resource: Transaction,
options: {
properties: {
price: {
type: 'currency',
},
phone: {
type: 'phone',
},
},
navigation: {
name: 'App',
icon: 'Settings',
}
}
};
```
### :white_check_mark: Select component available in ```adminjs-design-system```
The select component was extracted from the core package to ```adminjs-design-system```. You no longer need to implement such a component on your own.
================================================
FILE: bin/app.js
================================================
import bundler from '../lib/backend/bundler/app.bundler.js'
await bundler.build()
================================================
FILE: bin/globals.js
================================================
import bundler from '../lib/backend/bundler/globals.bundler.js'
await bundler.build()
================================================
FILE: cli.js
================================================
#!/usr/bin/env node
// eslint-disable-next-line
import('./lib/cli')
================================================
FILE: commitlint.config.cjs
================================================
module.exports = {
extends: [
'@commitlint/config-conventional',
],
}
================================================
FILE: cy/commands/ab-get-property.js
================================================
/// <reference types="cypress" />
/**
* @method abGetProperty
* @description
* ### Usage
*
* ```javascript
* // your property in AdminJS
* resourceOptions: {
* properties: {
* isAdmin: {...}
* }
* }
* ```
*
* ```javascript
* // accessing it in test.
* cy.abGetProperty('isAdmin', 'input[type="checkbox"]')
* ```
*
* `abGetProperty` returns the wrapper Section for a given property. You can pass inner element
* which allows you to select `input`, `label`, `options`, etc. inside it.
*
* @memberof module:cy
* @param {string} path path of the property
* @param {string} [selector=null]
* @example
* it('shows disabled checkBox', () => {
* cy.abGetProperty('isAdmin', 'input[type="checkbox"]')
* .should('be.disabled')
* .should('not.be.checked')
*
* cy.abGetProperty('isAdmin', 'label')
* .click()
* .should('not.be.checked')
* })
*/
Cypress.Commands.add('abGetProperty', (path, selector = null) => {
let propertySelector = `[data-testid$="-${path}"]`
if (selector) {
propertySelector = [propertySelector, selector].join(' ')
}
return cy.get(propertySelector)
})
================================================
FILE: cy/commands/ab-keep-logged-in.js
================================================
/// <reference types="cypress" />
/**
* @method abKeepLoggedIn
* @memberof module:cy
* @param {object} [options]
* @param {object} [options.cookie] session cookie name: default to Cypress.env('AB_COOKIE_NAME')
* @example
* before(() => {
* cy.abLogin()
* })
*
* beforeAll(() => {
* cy.abKeepLoggedIn({ cookie: 'my-session-cookie' })
* cy.visit('your/path')
* })
*/
Cypress.Commands.add('abKeepLoggedIn', ({ cookie }) => {
Cypress.Cookies.preserveOnce(cookie || Cypress.env('AB_COOKIE_NAME'))
})
================================================
FILE: cy/commands/ab-login-api.js
================================================
/// <reference types="cypress" />
/**
* @method abLoginAPI
* @memberof module:cy
* @description
* Comparing to {@link module:cy.abLogin} it doesn't render page - just performs an API call
* and logs you in by storing the cookie. In order to preserve this cookie between the `it()`
* calls you have to use {@link module:cy.abKeepLoggedIn} helper.
* @param {object} [options]
* @param {object} [options.email] login email: default to Cypress.env('AB_EMAIL')
* @param {object} [options.password] login password: default to Cypress.env('AB_PASSWORD')
* @param {object} [options.loginPath] default to '/login'
*/
Cypress.Commands.add('abLoginAPI', ({ email, password, loginPath } = {}) => (
cy.request('POST', loginPath || '/login', {
email: email || Cypress.env('AB_EMAIL'),
password: password || Cypress.env('AB_PASSWORD'),
})
))
================================================
FILE: cy/commands/ab-login.js
================================================
/// <reference types="cypress" />
/**
* @method abLogin
* @description
* logs you to the AdminJS. Since the system uses cookie for storing the session information, you
* can use {@link module:cy.abKeepLoggedIn} helper to keep it between test cases.
* @memberof module:cy
* @param {object} [options]
* @param {object} [options.email] login email: default to Cypress.env('AB_EMAIL')
* @param {object} [options.password] login password: default to Cypress.env('AB_PASSWORD')
* @param {object} [options.loginPath] default to '/login'
*/
Cypress.Commands.add('abLogin', ({ email, password, loginPath } = {}) => {
cy.visit(loginPath || '/login')
cy.get('[name=email]').type(email || Cypress.env('AB_EMAIL'))
cy.get('[name=password]').type(password || Cypress.env('AB_PASSWORD'))
cy.get('button').click()
})
================================================
FILE: cy/cypress.doc.md
================================================
### Cypress helpers
This module gathers helpers which can be used when you E2E test your AdminJS dashboard with
{@link https://www.cypress.io/} as we do.
### Usage
First, you have to import helpers to your cypress project. You can do this in:
`/support/index.js` or `/support/commands.js`
```javascript
require('adminjs/cy')
```
and now you can use our helpers
```javascript
/// <reference types="cypress" />
/// <reference types="adminjs/cy" />
context('resources/Company/actions/new', () => {
before(() => {
cy.abLoginAPI({ password: Cypress.env('ADMIN_PASSWORD'), email: Cypress.env('ADMIN_EMAIL') })
})
beforeEach(() => {
cy.abKeepLoggedIn({ cookie: Cypress.env('COOKIE_NAME') })
cy.visit('resources/Company/actions/new')
})
//...
})
```
### What we have
Cypress helpers project is currently in the WIP/POC phase, that is why there are not much helpers
here. But you can expect that gradually we will add more.
================================================
FILE: cy/index.d.ts
================================================
/// <reference types="cypress" />
declare namespace Cypress {
type AbLoginParams = {
email?: string;
password?: string;
loginPath?: string;
}
type AbKeepLoggedInParams = {
cookie?: string;
}
interface Chainable<Subject> {
abLogin(params?: AbLoginParams): Chainable<any>;
abLoginAPI(params?: AbLoginParams): Chainable<any>;
abGetProperty(propertyPath: string, selector?: string): Chainable<any>;
abKeepLoggedIn(params?: AbKeepLoggedInParams): Chainable<any>;
}
}
================================================
FILE: cy/index.js
================================================
/**
* @module cy
* @load ./cypress.doc.md
*/
import './commands/ab-login.js'
import './commands/ab-login-api.js'
import './commands/ab-keep-logged-in.js'
import './commands/ab-get-property.js'
================================================
FILE: cy/readme.md
================================================
## Test suite: Login page form
### LPF-1: Log in to the app with a valid email and password
**Test description:** Verify if a user will be able to log in with a valid email and password
**Type:** Functional
**Priority:** High
**Severity:** Critical
**Behavior:** Positive
**Automation status:** To be automated
**Tags:** login_page, login_form
#### Preconditions
* Open a login page in a browser([https://demo.adminjs.co/admin](https://demo.adminjs.co/admin))
* User is not already logged and has got an active account
<table>
<tr>
<td>
<span style="text-decoration:underline;">No.</span>
</td>
<td><span style="text-decoration:underline;">Steps</span>
</td>
<td><span style="text-decoration:underline;">Data</span>
</td>
<td><span style="text-decoration:underline;">Expected results</span>
</td>
</tr>
<tr>
<td>1
</td>
<td>Enter a valid email address in the “Email” input element
</td>
<td><em>test@example.com</em>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>2
</td>
<td>Enter a valid password in the “Password” input element
</td>
<td><em>password</em>
</td>
<td>The field has been completed with “*” signs
</td>
</tr>
<tr>
<td>3
</td>
<td>Click on the “Login” button
</td>
<td>
</td>
<td>Login should be successful
</td>
</tr>
<tr>
<td>4
</td>
<td>Check the URL address
</td>
<td>
</td>
<td>User should be on the “...<em>/admin</em>” page
</td>
</tr>
</table>
## Test suite: Mongoose Resources forms
### MRF-1: Create a new element via form on the “Complicated” list
**Test description:** Verify if a user will be able to create a new element on the list
**Type:** Functional
**Priority:** Medium
**Severity:** Normal
**Behavior:** Positive
**Automation status:** To be automated
**Tags:** mongoose_resources, complicated_category, complicated_form
#### Precondition:
* User is already logged into the application
<table>
<tr>
<td>
<span style="text-decoration:underline;">No.</span>
</td>
<td><span style="text-decoration:underline;">Steps</span>
</td>
<td><span style="text-decoration:underline;">Data</span>
</td>
<td><span style="text-decoration:underline;">Expected results</span>
</td>
</tr>
<tr>
<td>1
</td>
<td>Click on the hamburger menu if the navigation panel is not visible
</td>
<td>
</td>
<td>Navigation panel is visible
</td>
</tr>
<tr>
<td>2
</td>
<td>Click on the “Mongoose Resources” → Complicated link inside the navigation panel
</td>
<td>
</td>
<td>User should be redirected to the “<em>.../admin/resources/Complicated</em>” page
</td>
</tr>
<tr>
<td>3
</td>
<td>Hide the navigation panel if you launch it via hamburger menu
</td>
<td>
</td>
<td>Navigation panel is not visible. Hamburger menu is visible
</td>
</tr>
<tr>
<td>4
</td>
<td>Click on the “Create new” button
</td>
<td>
</td>
<td>The form is visible. User should be on the “<em>.../admin/resources/Complicated/actions/new</em>” page
</td>
</tr>
<tr>
<td>5
</td>
<td>Fill the “Name” input element with a random value
</td>
<td><em>e.g. Alex</em>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>6
</td>
<td>Click on the “Add New Item” button in the “String Array” section
</td>
<td>
</td>
<td>A text input element and bin icon are shown
</td>
</tr>
<tr>
<td>7
</td>
<td>Click on the bin icon
</td>
<td>
</td>
<td>The text input element was removed
</td>
</tr>
<tr>
<td>8
</td>
<td>Click on the “Add New Item” button again in the “String Array” section
</td>
<td>
</td>
<td>A text input element and bin icon are shown again
</td>
</tr>
<tr>
<td>9
</td>
<td>Fill the text input element with a random value
</td>
<td><em>e.g. String Array</em>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>10
</td>
<td>Click on the “Add New Item” button in the “Authors” section
</td>
<td>
</td>
<td>A text input element and bin icon are shown
</td>
</tr>
<tr>
<td>11
</td>
<td>Choose one of the elements from the dropdown list
</td>
<td><em>e.g. Books</em>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>12
</td>
<td>Fill the “Nested Details Age” input element with a random value
</td>
<td><em>e.g. 26</em>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>13
</td>
<td>Fill the “Nested Details Height” input element with a random value
</td>
<td><em>e.g. 187</em>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>14
</td>
<td>Fill the “Nested Details Place Of Birth” input element with a random value
</td>
<td><em>e.g. Warsaw</em>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>15
</td>
<td>Fill the “Nested Details Place Of Birth” input element with a random value
</td>
<td><em>e.g. Extremely Nested Text</em>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>16
</td>
<td>Click on the “Add New Item” button in the “Parents” section
</td>
<td>
</td>
<td>Two text input elements (“Parents Name”, “Parents Surname”) and the bin icon are shown
</td>
</tr>
<tr>
<td>17
</td>
<td>Fill the “Parents Name” input element with a random value
</td>
<td><em>e.g. Harry</em>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>18
</td>
<td>Fill the “Parents Surname” input element with a random value
</td>
<td><em>e.g. Potter</em>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>19
</td>
<td>Click on the “Add New Item” button in the “Item” section
</td>
<td>
</td>
<td>The “Item Image Variants” section with a new button “Add New Item” and the bin icon are shown
</td>
</tr>
<tr>
<td>20
</td>
<td>Click on the “Add New Item” button in the “Item Image Variants” section
</td>
<td>
</td>
<td>Two input elements, two checkboxes and a new bin icon are shown
</td>
</tr>
<tr>
<td>21
</td>
<td>Fill the “Item Image Variants Image URL” input element with a random value
</td>
<td><em>e.g. www.google.com</em>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>22
</td>
<td>Check the checkbox “Item Image Variants Is Approved”
</td>
<td>
</td>
<td>The checkbox is checked
</td>
</tr>
<tr>
<td>23
</td>
<td>Set the random date and time from the picker in “Item Image Variants Date Created” input element
</td>
<td>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>24
</td>
<td>Click on the “Save” button
</td>
<td>
</td>
<td>User is redirected to the “<em>.../admin/resources/Complicated</em>” page. The toast message: “Successfully created a new record” is shown
</td>
</tr>
<tr>
<td>25
</td>
<td>Look at the number of the elements on the list
</td>
<td>
</td>
<td>The number is increased by 1
</td>
</tr>
<tr>
<td>26
</td>
<td>Look at your new element on the list
</td>
<td>
</td>
<td>Your element should be at the top of the list
</td>
</tr>
<tr>
<td>27
</td>
<td>Look at values in each column in your element
</td>
<td>
</td>
<td>Users should see provided data in each related column. Columns “String Array”, “Authors”, “Parents” and “Item” show information about quantity. In this case “length: 1”. Column “Id” has a random string. Column “Updated At” shows time and date of creating
</td>
</tr>
</table>
## Test suite: Sequelize Resources filters
### SRF-1: Filter elements on the “Favorite Places” list
**Test description:** Verify if a user will be able to filter elements on the list
**Type:** Functional
**Priority:** Medium
**Severity:** Normal
**Behavior:** Positive
**Automation status:** To be automated
**Tags:** sequelize_resources, favorite_places_category, favorite_places_filters, filters
#### Precondition
* User is already logged into the application
<table>
<tr>
<td>
<span style="text-decoration:underline;">No.</span>
</td>
<td><span style="text-decoration:underline;">Steps</span>
</td>
<td><span style="text-decoration:underline;">Data</span>
</td>
<td><span style="text-decoration:underline;">Expected results</span>
</td>
</tr>
<tr>
<td>1
</td>
<td>Click on the hamburger menu if the navigation panel is not visible
</td>
<td>
</td>
<td>Navigation panel is visible
</td>
</tr>
<tr>
<td>2
</td>
<td>Click on the “Sequelize Resources” → Favourite Places link inside the navigation panel
</td>
<td>
</td>
<td>User should be redirected to the “<em>.../admin/resources/FavouritePlaces</em>” page
</td>
</tr>
<tr>
<td>3
</td>
<td>Hide the navigation panel if you launch it via hamburger menu
</td>
<td>
</td>
<td>Navigation panel is not visible. Hamburger menu is visible
</td>
</tr>
<tr>
<td>4
</td>
<td>If there is no any elements on the list, create at least two elements with random data
</td>
<td>
</td>
<td>The elements are visible on the list
</td>
</tr>
<tr>
<td>5
</td>
<td>Click on the “Filter” button
</td>
<td>
</td>
<td>The filters section with form is visible
</td>
</tr>
<tr>
<td>6
</td>
<td>Fill the “Name” input element with the name from e.g. the first element on the list
</td>
<td>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>7
</td>
<td>Click on the “Apply changes” button inside the filters section
</td>
<td>
</td>
<td>On the list should be visible only elements with the name inserted in the filter input
</td>
</tr>
<tr>
<td>8
</td>
<td>Click on the “Reset” button inside the filters section
</td>
<td>
</td>
<td>Inside the list all elements should be visible
</td>
</tr>
<tr>
<td>9
</td>
<td>Fill the “Id” input element with the Id from e.g. the first element on the list
</td>
<td>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>10
</td>
<td>Click on the “Apply changes” button inside the filters section
</td>
<td>
</td>
<td>On the list should be visible only elements with the Id inserted in the filter input
</td>
</tr>
<tr>
<td>11
</td>
<td>Click on the “Reset” button inside the filters section
</td>
<td>
</td>
<td>Inside the list all elements should be visible
</td>
</tr>
<tr>
<td>12
</td>
<td>Choose one of the elements from the “User Id” dropdown element with the User Id from e.g. the first element on the list
</td>
<td>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>13
</td>
<td>Click on the “Apply changes” button inside the filters section
</td>
<td>
</td>
<td>On the list should be visible only elements with the User Id inserted in the filter input
</td>
</tr>
<tr>
<td>14
</td>
<td>Click on the “Reset” button inside the filters section
</td>
<td>
</td>
<td>Inside the list all elements should be visible
</td>
</tr>
<tr>
<td>15
</td>
<td>Choose the dates “From” and “To” that first element’s “published At” field on the list is in the range of them
</td>
<td>
</td>
<td>The fields have been completed
</td>
</tr>
<tr>
<td>16
</td>
<td>Click on the “Apply changes” button inside he filters section
</td>
<td>
</td>
<td>On the list should be visible only elements with “Published At” date and time in the range of the filters
</td>
</tr>
<tr>
<td>17
</td>
<td>Click on the “Reset” button inside the filters section
</td>
<td>
</td>
<td>Inside the list all elements should be visible
</td>
</tr>
<tr>
<td>18
</td>
<td>Fill the “Description” input element with the word from the column “Description” from e.g. the first element on the list
</td>
<td>
</td>
<td>The field has been completed
</td>
</tr>
<tr>
<td>19
</td>
<td>Click on the “Apply changes” button inside the filters section
</td>
<td>
</td>
<td>On the list should be visible only elements with “Description” field which include the word from the filter input element
</td>
</tr>
<tr>
<td>20
</td>
<td>Click on the “Reset” button inside the filters section
</td>
<td>
</td>
<td>Inside the list all elements should be visible
</td>
</tr>
</table>
================================================
FILE: index.d.ts
================================================
import AdminJS from './types/src/index.js'
import { ReduxState } from './types/src/frontend/store/store.js'
export * from './types/src/index.js'
export {
AdminJS,
ReduxState,
}
export default AdminJS
declare const REDUX_STATE: ReduxState
================================================
FILE: index.js
================================================
import AdminJS from './lib/index.js'
export * from './lib/index.js'
export {
AdminJS,
}
export default AdminJS
================================================
FILE: package.json
================================================
{
"name": "adminjs",
"version": "7.8.17",
"description": "Admin panel for apps written in node.js",
"type": "module",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js",
"require": "./index.js"
},
"./bundler": "./lib/backend/bundler/index.js"
},
"scripts": {
"test": "mocha --loader=ts-node/esm ./spec/index.js",
"types": "tsc",
"clean": "rm -rf lib && mkdir lib && rm -fr types && mkdir types",
"build": "babel src --out-dir lib --copy-files --extensions '.ts,.js,.jsx,.tsx'",
"lint": "eslint './spec/**/*' './src/**/*' './cy/**/*' './*'",
"cover": "NODE_ENV=test nyc --reporter=lcov --reporter=text-lcov npm test",
"codecov": "NODE_ENV=test nyc --reporter=text-lcov npm test | codecov --pipe",
"bundle": "node bin/app.js",
"bundle:globals": "node bin/globals.js",
"cspell": "cspell src/**/*.ts src/**/*.js src/**/*.tsx src/**/*.jsx",
"check:all": "yarn types && yarn cspell && yarn lint && yarn test && yarn bundle && NODE_ENV=production yarn bundle",
"dev": "yarn build && yarn types && yarn bundle:globals && yarn bundle && NODE_ENV=production yarn bundle:globals && NODE_ENV=production yarn bundle",
"release": "semantic-release"
},
"bin": {
"admin": "./cli.js"
},
"nyc": {
"exclude": [
"spec",
"example-app",
"src/**/*.spec.ts",
"src/**/*.spec.js",
"src/**/*.spec.tsx",
"src/**/*.spec.jsx",
"src/**/*.factory.ts",
"src/backend/bundler/user-components-bundler.ts",
"docs",
"coverage",
"types",
"src/frontend/assets/scripts",
"lib",
".vscode",
".github",
"**/*.spec.js"
],
"all": true,
"extension": [
".js",
".jsx",
".tsx",
".ts"
]
},
"repository": {
"type": "git",
"url": "https://github.com/SoftwareBrothers/adminjs.git"
},
"engines": {
"node": ">=16.0.0"
},
"keywords": [
"hapi",
"express",
"mongoose",
"admin",
"admin-panel"
],
"browserslist": [
"last 5 Chrome versions"
],
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"author": "Wojciech Krysiak",
"license": "MIT",
"bugs": {
"url": "https://github.com/SoftwareBrothers/adminjs/issues"
},
"homepage": "https://github.com/SoftwareBrothers/adminjs#readme",
"dependencies": {
"@adminjs/design-system": "^4.1.0",
"@babel/core": "^7.23.9",
"@babel/parser": "^7.23.9",
"@babel/plugin-syntax-import-assertions": "^7.23.3",
"@babel/plugin-transform-runtime": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@babel/register": "^7.23.7",
"@hello-pangea/dnd": "^16.2.0",
"@redux-devtools/extension": "^3.2.5",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.5",
"axios": "^1.3.4",
"commander": "^10.0.0",
"flat": "^5.0.2",
"i18next": "^22.4.13",
"i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.2.0",
"lodash": "^4.17.21",
"ora": "^6.2.0",
"prop-types": "^15.8.1",
"punycode": "^2.3.0",
"qs": "^6.11.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-feather": "^2.0.10",
"react-i18next": "^12.2.0",
"react-is": "^18.2.0",
"react-redux": "^8.0.5",
"react-router": "^6.9.0",
"react-router-dom": "^6.9.0",
"redux": "^4.2.1",
"regenerator-runtime": "^0.14.1",
"rollup": "^4.11.0",
"rollup-plugin-esbuild-minify": "^1.1.1",
"rollup-plugin-polyfill-node": "^0.13.0",
"slash": "^5.0.0",
"uuid": "^9.0.0",
"xss": "^1.0.14"
},
"devDependencies": {
"@babel/cli": "^7.23.9",
"@commitlint/cli": "^17.5.0",
"@commitlint/config-conventional": "^17.4.4",
"@semantic-release/git": "^10.0.1",
"@testing-library/react": "^14.0.0",
"@types/babel-core": "^6.25.10",
"@types/chai": "^4.3.4",
"@types/chai-as-promised": "^7.1.5",
"@types/factory-girl": "^5.0.8",
"@types/flat": "^5.0.2",
"@types/lodash": "^4.14.194",
"@types/mocha": "^10.0.1",
"@types/node": "^20.6.0",
"@types/qs": "^6.9.7",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"@types/sinon": "^10.0.13",
"@types/sinon-chai": "^3.2.9",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"babel-plugin-module-resolver": "^5.0.0",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-change": "^2.1.2",
"core-js": "^3.36.0",
"cspell": "^6.30.2",
"eslint": "^8.36.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"factory-girl": "^5.0.4",
"husky": "^8.0.3",
"jsdom": "^21.1.1",
"jsdom-global": "^3.0.2",
"mocha": "^10.2.0",
"node-esm-import-all": "^1.0.0",
"npm-run-all": "^4.1.5",
"semantic-release": "^20.1.3",
"sinon": "^15.0.2",
"sinon-chai": "^3.7.0",
"ts-node": "10.8.1",
"typescript": "^5.3.3"
},
"resolutions": {
"react-redux": "8.0.5",
"redux": "4.2.1"
}
}
================================================
FILE: project.inlang/project_id
================================================
ed92cfe9d9d26d48639c434e4f5ddd645dfbd5ecaac14329f8b042b5546fd3a9
================================================
FILE: project.inlang/settings.json
================================================
{
"$schema": "https://inlang.com/schema/project-settings",
"sourceLanguageTag": "en",
"languageTags": [
"en", "es", "it", "ja", "pl", "pt-BR", "ua", "zh-CN"
],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-i18next@4/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-identical-pattern@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js"
],
"plugin.inlang.i18next": {
"pathPattern": "./src/locale/{languageTag}/translation.json"
}
}
================================================
FILE: spec/backend/helpers/helper-stub.ts
================================================
import sinon from 'sinon'
import ViewHelpers from '../../../src/backend/utils/view-helpers/view-helpers.js'
export const expectedResult = {
recordActionUrl: '#recordActionUrl',
resourceActionUrl: '#resourceActionUrl',
bulkActionUrl: '#bulkActionUrl',
loginUrl: 'loginUrl',
logoutUrl: 'logoutUrl',
rootUrl: 'admin',
assetPath: 'assetPath',
resourceUrl: 'resourceUrl',
dashboardUrl: 'dashboardUrl',
pageUrl: 'pageUrl',
editUrl: 'editUrl',
showUrl: 'showUrl',
deleteUrl: 'deleteUrl',
newUrl: 'newUrl',
listUrl: 'listUrl',
bulkDeleteUrl: 'bulkDeleteUrl',
}
export default (): ViewHelpers => (
{
options: {
loginPath: expectedResult.loginUrl,
logoutPath: expectedResult.logoutUrl,
rootPath: expectedResult.rootUrl,
},
recordActionUrl: sinon.stub().returns(expectedResult.recordActionUrl),
resourceActionUrl: sinon.stub().returns(expectedResult.resourceActionUrl),
bulkActionUrl: sinon.stub().returns(expectedResult.bulkActionUrl),
urlBuilder: sinon.stub(),
loginUrl: sinon.stub().returns(expectedResult.loginUrl),
logoutUrl: sinon.stub().returns(expectedResult.logoutUrl),
assetPath: sinon.stub().returns(expectedResult.assetPath),
resourceUrl: sinon.stub().returns(expectedResult.resourceUrl),
dashboardUrl: sinon.stub().returns(expectedResult.dashboardUrl),
pageUrl: sinon.stub().returns(expectedResult.pageUrl),
editUrl: sinon.stub().returns(expectedResult.editUrl),
showUrl: sinon.stub().returns(expectedResult.showUrl),
deleteUrl: sinon.stub().returns(expectedResult.deleteUrl),
newUrl: sinon.stub().returns(expectedResult.newUrl),
listUrl: sinon.stub().returns(expectedResult.listUrl),
bulkDeleteUrl: sinon.stub().returns(expectedResult.bulkDeleteUrl),
}
)
================================================
FILE: spec/backend/helpers/resource-stub.ts
================================================
import sinon from 'sinon'
import BaseProperty from '../../../src/backend/adapters/property/base-property.js'
import BaseResource from '../../../src/backend/adapters/resource/base-resource.js'
import ResourceDecorator from '../../../src/backend/decorators/resource/resource-decorator.js'
/**
* returns properties with following absolute paths:
* - normal: number
* - nested: mixed
* - nested.normal: string
* - nested.nested: mixed
* - nested.nested.normalInner: string
* - arrayed: string (array)
* - arrayedMixed: mixed (array)
* - arrayedMixed.arrayParam: string
*
* @private
*/
const buildProperties = (): Array<BaseProperty> => {
const normalProperty = new BaseProperty({ path: 'normal', type: 'number' }) as any
const nestedProperty = new BaseProperty({ path: 'nested', type: 'mixed' }) as any
const nested2Property = new BaseProperty({ path: 'nested', type: 'mixed' }) as any
const arrayProperty = new BaseProperty({ path: 'arrayed', type: 'string' }) as any
const arrayMixedProperty = new BaseProperty({ path: 'arrayedMixed', type: 'mixed' }) as any
arrayProperty.isArray = (): boolean => true
arrayMixedProperty.isArray = (): boolean => true
nestedProperty.subProperties = (): Array<BaseProperty> => [
new BaseProperty({ path: 'normal', type: 'string' }),
nested2Property,
]
nested2Property.subProperties = (): Array<BaseProperty> => [
new BaseProperty({ path: 'normalInner', type: 'string' }),
]
arrayMixedProperty.subProperties = (): Array<BaseProperty> => [
new BaseProperty({ path: 'arrayParam', type: 'string' }),
]
return [normalProperty, nestedProperty, arrayProperty, arrayMixedProperty]
}
export const expectedResult = {
id: 'someID',
properties: buildProperties(),
resourceName: 'resourceName',
databaseName: 'databaseName',
databaseType: 'mongodb',
parent: {
name: 'databaseName',
icon: 'icon-mongodb',
},
}
export default (): BaseResource => ({
_decorated: {} as ResourceDecorator,
id: sinon.stub().returns(expectedResult.id),
properties: sinon.stub().returns(expectedResult.properties),
property: sinon.stub().returns(new BaseProperty({ path: 'prop', type: 'string' })),
databaseName: sinon.stub().returns(expectedResult.databaseName),
databaseType: sinon.stub().returns(expectedResult.databaseType),
count: sinon.stub(),
find: sinon.stub(),
findOne: sinon.stub(),
findMany: sinon.stub(),
build: sinon.stub(),
create: sinon.stub(),
update: sinon.stub(),
delete: sinon.stub(),
assignDecorator: sinon.stub(),
decorate: sinon.stub(),
})
================================================
FILE: spec/backend/index.js
================================================
/* eslint-disable import/first */
import { factory } from 'factory-girl'
process.env.MONGO_URL = 'mongodb://mongo/admin-server-test'
global.factory = factory
import './adapters/base-record.spec.js'
import './bundler/generate-user-component-entry.spec.js'
import './decorators/property-decorator.spec.js'
import './decorators/resource-decorator.spec.js'
import './utils/populator.spec.js'
import './utils/resources-factory.spec.js'
================================================
FILE: spec/fixtures/action.factory.js
================================================
import { factory } from 'factory-girl'
factory.define('action', Object, {
name: factory.sequence('action.name', (n) => `action${n}`),
actionType: 'record',
icon: 'icon',
label: factory.sequence('action.label', (n) => `action label ${n}`),
guard: null,
showFilter: false,
component: undefined,
})
factory.extend('action', 'recordAction', {
actionType: 'record',
})
factory.extend('action', 'resourceAction', {
actionType: 'resource',
})
================================================
FILE: spec/fixtures/example-component.js
================================================
import React from 'react'
function ExampleComponent() {
return <div>Some example text</div>
}
export default ExampleComponent
================================================
FILE: spec/fixtures/property.factory.js
================================================
import { factory } from 'factory-girl'
factory.define('property', Object, {
isId: false,
isSortable: true,
isTitle: true,
label: factory.sequence('property.label', (n) => `some property ${n}`),
name: factory.sequence('property.name', (n) => `someProperty${n}`),
position: factory.sequence('property.position', (n) => n),
type: 'string',
})
================================================
FILE: spec/fixtures/record.factory.js
================================================
import { factory } from 'factory-girl'
factory.define('record', Object, {
params: {
param1: 'value',
'nested.param': 'value2',
_id: '5d6165fc1af7720536be0930',
},
populated: {},
errors: {},
id: '5d6165fc1af7720536be0930',
})
================================================
FILE: spec/index.js
================================================
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable import/first */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable func-names */
import * as url from 'url'
import path from 'path'
import register from '@babel/register'
import { importAll } from 'node-esm-import-all'
import presetReact from '@babel/preset-react'
import presetEnv from '@babel/preset-env'
import presetTs from '@babel/preset-typescript'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
register({
presets: [
presetReact,
[presetEnv, {
targets: {
node: '18',
},
modules: false,
loose: true,
}],
presetTs,
],
extensions: ['.jsx', '.js', '.ts', '.tsx'],
only: ['src/', 'spec/'],
})
import './setup.js'
await importAll({
dirname: path.join(__dirname, '/../src'),
filter: /spec\.(js|ts|tsx)$/i,
recursive: true,
})
================================================
FILE: spec/lib.js
================================================
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable func-names */
import { importAll } from 'node-esm-import-all'
import './setup.js'
await importAll({
dirname: '../lib/',
filter: /spec\.js$/i,
recursive: true,
})
================================================
FILE: spec/setup.js
================================================
/* eslint-disable func-names */
/* eslint-disable mocha/no-top-level-hooks */
import { URLSearchParams } from 'url'
import chai from 'chai'
import sinonChai from 'sinon-chai'
import sinon from 'sinon'
import jsdom from 'jsdom-global'
process.env.NODE_ENV = 'test'
chai.use(sinonChai)
global.expect = chai.expect
global.URLSearchParams = URLSearchParams
beforeEach(function () {
this.sinon = sinon.createSandbox()
this.jsdom = jsdom()
})
afterEach(function () {
this.sinon.restore()
})
================================================
FILE: src/adminjs-options.interface.ts
================================================
import { ThemeOverride } from '@adminjs/design-system'
import type { TransformOptions as BabelConfig } from 'babel-core'
import AdminJS from './adminjs.js'
import BaseResource from './backend/adapters/resource/base-resource.js'
import BaseDatabase from './backend/adapters/database/base-database.js'
import { PageContext } from './backend/actions/action.interface.js'
import { ResourceOptions } from './backend/decorators/resource/resource-options.interface.js'
import { Locale } from './locale/config.js'
import { CurrentAdmin } from './current-admin.interface.js'
import { CoreScripts } from './core-scripts.interface.js'
import { ComponentLoader } from './backend/utils/component-loader.js'
/**
* AdminJSOptions
*
* This is the heart of entire AdminJS - all options resides here.
*
* ### Usage with regular javascript
*
* ```javascript
* const AdminJS = require('adminjs')
* //...
* const adminJS = new AdminJS({
* rootPath: '/xyz-admin',
* logoutPath: '/xyz-admin/exit',
* loginPath: '/xyz-admin/sign-in',
* databases: [mongooseConnection],
* resources: [{ resource: ArticleModel, options: {...}}],
* branding: {
* companyName: 'XYZ c.o.',
* },
* })
* ```
*
* ### TypeScript
*
* ```
* import { AdminJSOptions } from 'adminjs'
*
* const options: AdminJSOptions = {
* rootPath: '/xyz-admin',
* logoutPath: '/xyz-admin/exit',
* loginPath: '/xyz-admin/sign-in',
* databases: [mongooseConnection],
* resources: [{ resource: ArticleModel, options: {...}}],
* branding: {
* companyName: 'XYZ c.o.',
* },
* }
*
* const adminJs = new AdminJS(options)
* ```
*/
export interface AdminJSOptions {
/**
* path, under which, AdminJS will be available. Default to `/admin`
*
*/
rootPath?: string;
/**
* url to a logout action, default to `/admin/logout`
*/
logoutPath?: string;
/**
* url to a login page, default to `/admin/login`
*/
loginPath?: string;
/**
* Array of all Databases which are supported by AdminJS via adapters
*/
databases?: Array<any>;
componentLoader?: ComponentLoader;
/**
* List of custom pages which will be visible below all resources
* @example
* pages: {
* customPage: {
* label: "Custom page",
* handler: async (request, response, context) => {
* return {
* text: 'I am fetched from the backend',
* }
* },
* component: 'CustomPage',
* },
* anotherPage: {
* label: "TypeScript page",
* component: 'TestComponent',
* },
* },
*/
pages?: AdminPages;
/**
* Array of all Resources which are supported by AdminJS via adapters.
* You can pass either resource or resource with an options and thus modify it.
* @property {any} resources[].resource
* @property {ResourceOptions} resources[].options
* @property {Array<FeatureType>} resources[].features
*
* @see ResourceOptions
*/
resources?: Array<ResourceWithOptions | any>;
/**
* Option to modify the dashboard
*/
dashboard?: {
/**
* Handler function which can be triggered using {@link ApiClient#getDashboard}.
*/
handler?: PageHandler;
/**
* Bundled component name which should be rendered when user opens the dashboard
*/
component?: string;
};
/**
* Flag which indicates if version number should be visible on the UI
*/
version?: VersionSettings;
/**
* Options which are related to the branding.
*/
branding?: BrandingOptions | BrandingOptionsFunction;
/**
* Custom assets you want to pass to AdminJS
*/
assets?: Assets | AssetsFunction;
/**
* Indicates is bundled by AdminJS files like:
* - components.bundle.js
* - global.bundle.js
* - design-system.bundle.js
* - app.bundle.js
* should be taken from the same server as other AdminJS routes (default)
* or should be taken from an external CDN.
*
* If set - bundles will go from given CDN if unset - from the same server.
*
* When you can use this option? So let's say you want to deploy the app on
* serverless environment like Firebase Cloud Functions. In that case you don't
* want to serve static files with the same app because your function will be
* invoked every time frontend asks for static assets.
*
* Solution would be to:
* - create `public` folder in your app
* - generate `bundle.js` file to `.adminjs/` folder by using {@link AdminJS#initialize}
* function (with process.env.NODE_ENV set to 'production').
* - copy the before mentioned file to `public` folder and rename it to
* components.bundle.js
* - copy
* './node_modules/adminjs/lib/frontend/assets/scripts/app-bundle.production.js' to
* './public/app.bundle.js',
* - copy
* './node_modules/adminjs/lib/frontend/assets/scripts/global-bundle.production.js' to
* './public/global.bundle.js'
* * - copy
* './node_modules/adminjs/node_modules/@adminjs/design-system/bundle.production.js' to
* './public/design-system.bundle.js'
* - host entire public folder under some domain (if you use firebase - you can host them
* with firebase hosting)
* - point {@link AdminJS.assetsCDN} to this domain
*/
assetsCDN?: string;
/**
* Environmental variables passed to the frontend.
*
* So let say you want to pass some _GOOGLE_MAP_API_TOKEN_ to the frontend.
* You can do this by adding it here:
*
* ```javascript
* new AdminJS({env: {
* GOOGLE_MAP_API_TOKEN: 'my-token',
* }})
* ```
*
* and this token will be available on the frontend by using:
*
* ```javascript
* AdminJS.env.GOOGLE_MAP_API_TOKEN
* ```
*/
env?: Record<string, string>;
/**
* Translations
*
* Change it in order to:
* - localize admin panel
* - change any arbitrary text in the UI
*
* This is the example for changing name of a couple of resources along with some
* properties to Polish. You can also use this technic to change any text even in english.
* So to change button label of a "new action" from default "Create new" to "Create new Comment"
* only for Comment resource, place it in action section.
*
* ```javascript
* {
* locale: {
* translations: {
* pl: {
* labels: {
* Comments: "Komentarze",
* },
* resources: {
* Comments: {
* properties: {
* name: "Nazwa Komentarza",
* content: "Zawartość",
* },
* actions: {
* new: 'Create new Comment'
* }
* }
* }
* }
* }
* }
*}
* ```
*
* Check out the [i18n tutorial]{@tutorial i18n} to see how
* internationalization in AdminJS works.
*/
locale?: Locale | ((admin?: CurrentAdmin) => Locale | Promise<Locale>);
/**
* rollup bundle options;
*/
bundler?: BundlerOptions;
/**
* Additional settings.
*/
settings?: Partial<AdminJSSettings>;
/**
* List of available themes, for example exports of the `@adminjs/themes` npm package.
*/
availableThemes?: ThemeConfig[];
/**
* ID of the default theme. If not provided, the first theme from the `availableThemes`
* list will be used.
*/
defaultTheme?: string;
}
export type ThemeConfig = {
id: string,
name: string,
overrides: Partial<ThemeOverride>;
bundlePath?: string;
stylePath?: string;
}
export type AdminJSSettings = {
defaultPerPage: number;
};
/* cspell: enable */
/**
* @memberof AdminJSOptions
* @alias Assets
*
* Optional assets (stylesheets, and javascript libraries) which can be
* appended to the HEAD of the page.
*
* you can also pass {@link AssetsFunction} instead.
*/
export type Assets = {
/**
* List to urls of custom stylesheets. You can pass your font - icons here (as an example)
*/
styles?: Array<string>;
/**
* List of urls to custom scripts. If you use some particular js
* library - you can pass its url here.
*/
scripts?: Array<string>;
/**
* Mapping of core scripts in case you want to version your assets
*/
coreScripts?: CoreScripts;
};
/**
* @alias AssetsFunction
* @name AssetsFunction
* @memberof AdminJSOptions
* @returns {Assets | Promise<Assets>}
* @description
* Function returning {@link Assets}
*/
export type AssetsFunction = (admin?: CurrentAdmin) => Assets | Promise<Assets>;
/**
* Version Props
* @alias VersionProps
* @memberof AdminJSOptions
*/
export type VersionSettings = {
/**
* if set to true - current admin version will be visible
*/
admin?: boolean;
/**
* Here you can pass any arbitrary version text which will be seen in the US.
* You can pass here your current API version.
*/
app?: string;
};
export type VersionProps = {
admin?: string;
app?: string;
};
/**
* Branding Options
*
* You can use them to change how AdminJS looks. For instance to change name and
* colors (dark theme) run:
*
* ```javascript
* new AdminJS({
* branding: {
* companyName: 'John Doe Family Business',
* theme,
* }
* })
* ```
*
* @alias BrandingOptions
* @memberof AdminJSOptions
*/
export type BrandingOptions = {
/**
* URL to a logo, or `false` if you want to hide the default one.
*/
logo?: string | false;
/**
* Name of your company, which will replace "AdminJS".
*/
companyName?: string;
/**
* CSS theme.
*/
theme?: Partial<ThemeOverride>;
/**
* Flag indicates if "made with love" tiny heart icon
* should be visible on the bottom sidebar and login page.
* @new since 6.0.0
*/
withMadeWithLove?: boolean;
/**
* URL to a favicon
*/
favicon?: string;
};
/**
* Branding Options Function
*
* function returning BrandingOptions.
*
* @alias BrandingOptionsFunction
* @memberof AdminJSOptions
* @returns {BrandingOptions | Promise<BrandingOptions>}
*/
export type BrandingOptionsFunction = (
admin?: CurrentAdmin,
) => BrandingOptions | Promise<BrandingOptions>;
/**
* Object describing regular page in AdminJS
*
* @alias AdminPage
* @memberof AdminJSOptions
*/
export type AdminPage = {
/**
* Handler function
*/
handler?: PageHandler;
/**
* Component defined by using {@link ComponentLoader}
*/
component: string;
/**
* Page icon
*/
icon?: string;
};
/**
* Object describing map of regular pages in AdminJS
*
* @alias AdminPages
* @memberof AdminJSOptions
*/
export type AdminPages = Record<string, AdminPage>;
/**
* Default way of passing Options with a Resource
* @alias ResourceWithOptions
* @memberof AdminJSOptions
*/
export type ResourceWithOptions = {
resource: any;
options: ResourceOptions;
features?: Array<FeatureType>;
};
/**
* Function taking {@link ResourceOptions} and merging it with all other options
*
* @alias FeatureType
* @type function
* @returns {ResourceOptions}
* @memberof AdminJSOptions
*/
export type FeatureType = (
/**
* AdminJS instance
*/
admin: AdminJS,
/**
* Options returned by the feature added before
*/
options: ResourceOptions,
) => ResourceOptions;
/**
* Function which is invoked when user enters given AdminPage
*
* @alias PageHandler
* @memberof AdminJSOptions
*/
export type PageHandler = (request: any, response: any, context: PageContext) => Promise<any>;
/**
* Bundle options
*
* @alias BundlerOptions
* @memberof AdminJSOptions
* @example
* const adminJS = new AdminJS({
resources: [],
rootPath: '/admin',
babelConfig: './.adminJS.babelrc'
})
*/
export type BundlerOptions = {
/**
* The file path to babel config file or json object of babel config.
*/
babelConfig?: BabelConfig | string;
};
export interface AdminJSOptionsWithDefault extends AdminJSOptions {
rootPath: string;
logoutPath: string;
loginPath: string;
refreshTokenPath: string;
databases?: Array<BaseDatabase>;
resources?: Array<
| BaseResource
| {
resource: BaseResource;
options: ResourceOptions;
}
>;
dashboard: {
handler?: PageHandler;
component?: string;
};
bundler: BundlerOptions;
pages: AdminJSOptions['pages'];
}
================================================
FILE: src/adminjs.spec.ts
================================================
import path from 'path'
import { expect } from 'chai'
import * as url from 'url'
import AdminJS from './adminjs.js'
import BaseDatabase from './backend/adapters/database/base-database.js'
import BaseResource from './backend/adapters/resource/base-resource.js'
import { ComponentLoader } from './backend/utils/component-loader.js'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
describe('AdminJS', function () {
beforeEach(function () {
global.RegisteredAdapters = []
})
describe('#constructor', function () {
it('sets default root path when no given', function () {
expect(new AdminJS().options.rootPath).to.equal('/admin')
})
})
describe('.AdminJS.registerAdapter', function () {
beforeEach(function () {
class Database extends BaseDatabase {}
class Resource extends BaseResource {}
this.DatabaseAdapter = { Database, Resource }
})
it('adds given adapter to list off all available adapters', function () {
AdminJS.registerAdapter(this.DatabaseAdapter)
expect(global.RegisteredAdapters).to.have.lengthOf(1)
})
it('throws an error when adapter is not full', function () {
expect(() => {
AdminJS.registerAdapter({
Resource: BaseResource,
Database: null as unknown as typeof BaseDatabase })
}).to.throw('Adapter has to have both Database and Resource')
})
it('throws an error when adapter has elements not being subclassed from base adapter', function () {
expect(() => {
AdminJS.registerAdapter({
Resource: {} as typeof BaseResource,
Database: {} as typeof BaseDatabase,
})
}).to.throw('Adapter elements have to be a subclass of AdminJS.BaseResource and AdminJS.BaseDatabase')
})
})
describe('resolveBabelConfigPath', function () {
it('load .babelrc file', function () {
const adminJS = new AdminJS({ bundler: { babelConfig: '../.babelrc.json' } })
expect(adminJS.options.bundler.babelConfig).not.to.undefined
})
it('load with json object directly', function () {
const adminJS = new AdminJS({ bundler: { babelConfig: {
presets: [
'@babel/preset-react',
['@babel/preset-env', {
targets: {
node: '18',
},
modules: false,
loose: true,
}],
'@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',
],
} } })
expect(adminJS.options.bundler.babelConfig).not.to.undefined
})
it('load babel.config.cjs file', function () {
const adminJS = new AdminJS({ bundler: { babelConfig: './babel.test.config.json' } })
expect(adminJS.options.bundler.babelConfig).not.to.undefined
})
})
describe('ComponentLoader', function () {
const loader = new ComponentLoader()
afterEach(function () {
loader.clear()
})
context('file exists', function () {
beforeEach(function () {
this.result = loader.add('ExampleComponent', '../spec/fixtures/example-component')
})
it('adds given file to a UserComponents object', function () {
expect(Object.keys(loader.getComponents())).to.have.lengthOf(1)
})
it('returns uniqe id', function () {
expect(loader.getComponents()[this.result]).not.to.be.undefined
expect(this.result).to.be.a('string')
})
it('converts relative path to absolute path', function () {
expect(
loader.getComponents()[this.result],
).to.equal(path.join(__dirname, '../spec/fixtures/example-component'))
})
})
context('component name given', function () {
it('returns the same component name as which was given', function () {
const name = loader.add('Dashboard', '../spec/fixtures/example-component')
expect(name).to.eq('Dashboard')
})
})
it('throws an error when component doesn\'t exist', function () {
expect(() => {
loader.add('ExampleComponent', './fixtures/example-components')
}).to.throw().property('name', 'ConfigurationError')
})
})
})
================================================
FILE: src/adminjs.ts
================================================
import merge from 'lodash/merge.js'
import * as path from 'path'
import * as fs from 'fs'
import * as url from 'url'
import { AdminJSOptionsWithDefault, AdminJSOptions } from './adminjs-options.interface.js'
import BaseResource from './backend/adapters/resource/base-resource.js'
import BaseDatabase from './backend/adapters/database/base-database.js'
import ConfigurationError from './backend/utils/errors/configuration-error.js'
import ResourcesFactory from './backend/utils/resources-factory/resources-factory.js'
import componentsBundler from './backend/bundler/components.bundler.js'
import {
RecordActionResponse,
Action,
BulkActionResponse,
} from './backend/actions/action.interface.js'
import { DEFAULT_PATHS } from './constants.js'
import { ACTIONS } from './backend/actions/index.js'
import loginTemplate, { LoginTemplateAttributes } from './frontend/login-template.js'
import { ListActionResponse } from './backend/actions/list/list-action.js'
import { Locale } from './locale/index.js'
import { TranslateFunctions } from './utils/translate-functions.factory.js'
import { relativeFilePathResolver } from './utils/file-resolver.js'
import { Router } from './backend/utils/index.js'
import { ComponentLoader } from './backend/utils/component-loader.js'
import { bundlePath, stylePath } from './utils/theme-bundler.js'
import generateEntry from './backend/bundler/generate-user-component-entry.js'
import { ADMIN_JS_TMP_DIR } from './backend/bundler/utils/constants.js'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'))
export const VERSION = pkg.version
export const defaultOptions: AdminJSOptionsWithDefault = {
rootPath: DEFAULT_PATHS.rootPath,
logoutPath: DEFAULT_PATHS.logoutPath,
loginPath: DEFAULT_PATHS.loginPath,
refreshTokenPath: DEFAULT_PATHS.refreshTokenPath,
databases: [],
resources: [],
dashboard: {},
pages: {},
bundler: {},
}
type ActionsMap = {
show: Action<RecordActionResponse>
edit: Action<RecordActionResponse>
delete: Action<RecordActionResponse>
bulkDelete: Action<BulkActionResponse>
new: Action<RecordActionResponse>
list: Action<ListActionResponse>
}
export type Adapter = { Database: typeof BaseDatabase; Resource: typeof BaseResource }
/**
* Main class for AdminJS extension. It takes {@link AdminJSOptions} as a
* parameter and creates an admin instance.
*
* Its main responsibility is to fetch all the resources and/or databases given by a
* user. Its instance is a currier - injected in all other classes.
*
* @example
* const AdminJS = require('adminjs')
* const admin = new AdminJS(AdminJSOptions)
*/
class AdminJS {
public resources: Array<BaseResource>
public options: AdminJSOptionsWithDefault
public locale!: Locale
public translateFunctions!: TranslateFunctions
public componentLoader: ComponentLoader
/**
* List of all default actions. If you want to change the behavior for all actions like:
* _list_, _edit_, _show_, _delete_ and _bulkDelete_ you can do this here.
*
* @example <caption>Modifying accessibility rules for all show actions</caption>
* const { ACTIONS } = require('adminjs')
* ACTIONS.show.isAccessible = () => {...}
*/
public static ACTIONS: ActionsMap
/**
* AdminJS version
*/
public static VERSION: string
/**
* @param {AdminJSOptions} options Options passed to AdminJS
*/
constructor(options: AdminJSOptions = {}) {
/**
* @type {BaseResource[]}
* @description List of all resources available for the AdminJS.
* They can be fetched with the {@link AdminJS#findResource} method
*/
this.resources = []
/**
* @type {AdminJSOptions}
* @description Options given by a user
*/
this.options = merge({}, defaultOptions, options)
this.resolveBabelConfigPath()
const { databases, resources } = this.options
this.componentLoader = options.componentLoader ?? new ComponentLoader()
const resourcesFactory = new ResourcesFactory(this, global.RegisteredAdapters || [])
this.resources = resourcesFactory.buildResources({ databases, resources })
this.addThemeAssets()
}
/**
* Registers various database adapters written for AdminJS.
*
* @example
* const AdminJS = require('adminjs')
* const MongooseAdapter = require('adminjs-mongoose')
* AdminJS.registerAdapter(MongooseAdapter)
*
* @param {Object} options
* @param {typeof BaseDatabase} options.Database subclass of {@link BaseDatabase}
* @param {typeof BaseResource} options.Resource subclass of {@link BaseResource}
*/
static registerAdapter({
Database,
Resource,
}: {
Database: typeof BaseDatabase
Resource: typeof BaseResource
}): void {
if (!Database || !Resource) {
throw new Error('Adapter has to have both Database and Resource')
}
// TODO: check if this is actually valid because "isAdapterFor" is always defined.
// checking if both Database and Resource have at least isAdapterFor method
// @ts-ignore
if (Database.isAdapterFor && Resource.isAdapterFor) {
global.RegisteredAdapters = global.RegisteredAdapters || []
global.RegisteredAdapters.push({ Database, Resource })
} else {
throw new Error(
'Adapter elements have to be a subclass of AdminJS.BaseResource and AdminJS.BaseDatabase',
)
}
}
/**
* Initializes AdminJS instance in production. This function should be called by
* all external plugins.
*/
async initialize(): Promise<void> {
if (process.env.NODE_ENV === 'production' && !(process.env.ADMIN_JS_SKIP_BUNDLE === 'true')) {
// eslint-disable-next-line no-console
console.log('AdminJS: bundling user components...')
await componentsBundler.createEntry({
content: generateEntry(this, ADMIN_JS_TMP_DIR),
})
await componentsBundler.build()
}
}
/**
* Watches for local changes in files imported via {@link ComponentLoader}.
* It doesn't work on production environment.
*
* @return {Promise<never>}
*/
async watch(): Promise<string | undefined> {
if (process.env.NODE_ENV !== 'production') {
await componentsBundler.createEntry({
content: generateEntry(this, ADMIN_JS_TMP_DIR),
})
await componentsBundler.watch()
}
return undefined
}
/**
* Renders an entire login page with email and password fields
* using {@link Renderer}.
*
* Used by external plugins
*
* @param {Object} options
* @param {String} options.action Login form action url - it could be
* '/admin/login'
* @param {String} [options.errorMessage] Optional error message. When set,
* renderer will print this message in
* the form
* @return {Promise<string>} HTML of the rendered page
*/
async renderLogin(props: LoginTemplateAttributes): Promise<string> {
return loginTemplate(this, props)
}
/**
* Returns resource base on its ID
*
* @example
* const User = admin.findResource('users')
* await User.findOne(userId)
*
* @param {String} resourceId ID of a resource defined under {@link BaseResource#id}
* @return {BaseResource} found resource
* @throws {Error} When resource with given id cannot be found
*/
findResource(resourceId): BaseResource {
const resource = this.resources.find((m) => m._decorated?.id() === resourceId)
if (!resource) {
throw new Error(
[
`There are no resources with given id: "${resourceId}"`,
'This is the list of all registered resources you can use:',
this.resources.map((r) => r._decorated?.id() || r.id()).join(', '),
].join('\n'),
)
}
return resource
}
/**
* Resolve babel config file path,
* and load configuration to this.options.bundler.babelConfig.
*/
resolveBabelConfigPath(): void {
if (typeof this.options?.bundler?.babelConfig !== 'string') {
return
}
let filePath = ''
let config = this.options?.bundler?.babelConfig
if (config[0] === '/') {
filePath = config
} else {
filePath = relativeFilePathResolver(config, /new AdminJS/)
}
if (!fs.existsSync(filePath)) {
throw new ConfigurationError(
`Given babel config "${filePath}", doesn't exist.`,
'AdminJS.html',
)
}
if (path.extname(filePath) === '.js') {
// eslint-disable-next-line
const configModule = require(filePath)
// eslint-disable-next-line max-len
config = configModule && configModule.__esModule ? configModule.default || undefined : configModule
if (!config || typeof config !== 'object' || Array.isArray(config)) {
throw new Error(`${filePath}: Configuration should be an exported JavaScript object.`)
}
} else {
try {
config = JSON.parse(fs.readFileSync(filePath, 'utf8'))
} catch (err) {
throw new Error(`${filePath}: Error while parsing config - ${err.message}`)
}
if (!config) throw new Error(`${filePath}: No config detected`)
if (typeof config !== 'object') {
throw new Error(`${filePath}: Config returned typeof ${typeof config}`)
}
if (Array.isArray(config)) {
throw new Error(`${filePath}: Expected config object but found array`)
}
}
this.options.bundler.babelConfig = config
}
addThemeAssets() {
this.options.availableThemes?.forEach((theme) => {
Router.assets.push({
path: `/frontend/assets/themes/${theme.id}/theme.bundle.js`,
src: theme.bundlePath ?? bundlePath(theme.id),
})
Router.assets.push({
path: `/frontend/assets/themes/${theme.id}/style.css`,
src: theme.stylePath ?? stylePath(theme.id),
})
})
}
private static __unsafe_componentIndex = 0
public static __unsafe_staticComponentLoader = new ComponentLoader()
}
AdminJS.VERSION = VERSION
AdminJS.ACTIONS = ACTIONS
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface AdminJS extends TranslateFunctions {}
export const { registerAdapter } = AdminJS
export default AdminJS
================================================
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/backend/actions/action.interface.ts
================================================
import { IconProps, VariantType } from '@adminjs/design-system'
import AdminJS from '../../adminjs.js'
import { CurrentAdmin } from '../../current-admin.interface.js'
import ViewHelpers from '../utils/view-helpers/view-helpers.js'
import BaseRecord from '../adapters/record/base-record.js'
import BaseResource from '../adapters/resource/base-resource.js'
import ActionDecorator from '../decorators/action/action-decorator.js'
import { LayoutElement, LayoutElementFunction } from '../utils/layout-element-parser/index.js'
import { RecordJSON } from '../../frontend/interfaces/index.js'
import { type NoticeMessage } from '../../frontend/interfaces/noticeMessage.interface.js'
export type ActionQueryParameters = {
sortBy?: string
direction?: 'asc' | 'desc'
filters?: Record<string, unknown>
perPage?: number
page?: number
}
export type ActionType = 'resource' | 'record' | 'bulk'
/**
* Execution context for an action. It is passed to the {@link Action#handler},
* {@link Action#before} and {@link Action#after} functions.
*
* @memberof Action
* @alias ActionContext
*/
export type ActionContext = {
/**
* current instance of AdminJS. You may use it to fetch other Resources by their names:
*/
_admin: AdminJS;
/**
* Resource on which action has been invoked. Null for dashboard handler.
*/
resource: BaseResource;
/**
* Record on which action has been invoked (only for {@link actionType} === 'record')
*/
record?: BaseRecord;
/**
* Records on which action has been invoked (only for {@link actionType} === 'bulk')
*/
records?: Array<BaseRecord>;
/**
* view helpers
*/
h: ViewHelpers;
/**
* Object of currently invoked function. Not present for dashboard action
*/
action: ActionDecorator;
/**
* Currently logged in admin
*/
currentAdmin?: CurrentAdmin;
/**
* Any custom property which you can add to context
*/
[key: string]: any;
}
/**
* Context object passed to a PageHandler
*
* @alias PageContext
* @memberof AdminJSOptions
*/
export type PageContext = {
/**
* current instance of AdminJS. You may use it to fetch other Resources by their names:
*/
_admin: AdminJS;
/**
* Currently logged in admin
*/
currentAdmin?: CurrentAdmin;
/**
* view helpers
*/
h: ViewHelpers;
}
/**
* ActionRequest
* @memberof Action
* @alias ActionRequest
*/
export type ActionRequest = {
/**
* parameters passed in an URL
*/
params: {
/**
* Id of current resource
*/
resourceId: string;
/**
* Id of current record (in case of record action)
*/
recordId?: string;
/**
* Id of selected records (in case of bulk action) divided by commas
*/
recordIds?: string;
/**
* Name of an action
*/
action: string;
/**
* an optional search query string (for `search` resource action)
*/
query?: string;
[key: string]: any;
};
/**
* POST data passed to the backend
*/
payload?: Record<string, any>;
/**
* Elements of query string
*/
query?: Record<string, any>;
/**
* HTTP method
*/
method: 'post' | 'get';
}
/**
* Base response for all actions
* @memberof Action
* @alias ActionResponse
*/
export type ActionResponse = {
/**
* Notice message which should be presented to the end user after showing the action
*/
notice?: NoticeMessage;
/**
* redirect path
*/
redirectUrl?: string;
/**
* Any other custom parameter
*/
[key: string]: any;
}
/**
* @description
* Defines the type of {@link Action#isAccessible} and {@link Action#isVisible} functions
* @alias IsFunction
* @memberof Action
*/
export type IsFunction = (context: ActionContext) => boolean
/**
* Required response of a Record action. Extends {@link ActionResponse}
*
* @memberof Action
* @alias RecordActionResponse
*/
export type RecordActionResponse = ActionResponse & {
/**
* Record object.
*/
record: RecordJSON;
}
/**
* Required response of a Record action. Extends {@link ActionResponse}
*
* @memberof Action
* @alias RecordActionResponse
*/
export type BulkActionResponse = ActionResponse & {
/**
* Array of RecordJSON objects.
*/
records: Array<RecordJSON>;
}
/**
* Type of a handler function. It has to return response compatible
* with {@link ActionResponse}, {@link BulkActionResponse} or {@link RecordActionResponse}
*
* @alias ActionHandler
* @async
* @memberof Action
* @returns {T | Promise<T>}
*/
export type ActionHandler<T> = (
request: ActionRequest,
response: any,
context: ActionContext
) => T | Promise<T>
/**
* Before action hook. When it is given - it is performed before the {@link ActionHandler}
* method.
* @alias Before
* @returns {ActionRequest | Promise<ActionRequest>}
* @memberof Action
* @async
*/
export type Before = (
/**
* Request object
*/
request: ActionRequest,
/**
* Invocation context
*/
context: ActionContext,
) => ActionRequest | Promise<ActionRequest>
/**
* Type of an after hook action.
*
* @memberof Action
* @alias After
* @async
*/
export type After<T> = (
/**
* Response returned by the default ActionHandler
*/
response: T,
/**
* Original request which has been sent to ActionHandler
*/
request: ActionRequest,
/**
* Invocation context
*/
context: ActionContext,
) => T | Promise<T>
export type BuildInActions =
'show' |
'edit' |
'list' |
'delete' |
'bulkDelete' |
'new' |
'search'
/**
* @classdesc
* Interface representing an Action in AdminJS.
* Look at {@tutorial actions} to see where you can use this interface.
*
* #### Example Action
*
* ```
* const action = {
* actionType: 'record',
* icon: 'View',
* isVisible: true,
* handler: async () => {...},
* component: 'MyAction',
* }
* ```
*
* There are 3 kinds of actions:
*
* 1. Resource action, which is performed for an entire resource.
* 2. Record action, invoked for an record in a resource
* 3. Bulk action, invoked for an set of records in a resource
*
* ...and there are 7 actions predefined in AdminJS
*
* 1. {@link module:NewAction new} (resource action) - create new records in a resource
* 2. {@link module:ListAction list} (resource action) - list all records within a resource
* 3. {@link module:SearchAction search} (resource action) - search by query string
* 4. {@link module:EditAction edit} (record action) - update records in a resource
* 5. {@link module:ShowAction show} (record action) - show details of given record
* 6. {@link module:DeleteAction delete} (record action) - delete given record
* 7. {@link module:BulkDeleteAction bulkDelete} (bulk action) - delete given records
*
* Users can also create their own actions or override those already existing by using
* {@link ResourceOptions}
*
* ```javascript
* const AdminJSOptions = {
* resources: [{
* resource: User,
* options: {
* actions: {
* // example of overriding existing 'new' action for
* // User resource.
* new: {
* icon: 'Add'
* },
* // Example of creating a new 'myNewAction' which will be
* // a resource action available for User model
* myNewAction: {
* actionType: 'resource',
* handler: async (request, response, context) => {...}
* }
* }
* }
* }]
* }
*
* const { ACTIONS } = require('adminjs')
* // example of adding after filter for 'show' action for all resources
* ACTIONS.show.after = async () => {...}
* ```
*/
export interface Action <T extends ActionResponse> {
/**
* Name of an action which is its uniq key.
* If you use one of _list_, _search_, _edit_, _new_, _show_, _delete_ or
* _bulkDelete_ you override existing actions.
* For all other keys you create a new action.
*/
name: BuildInActions | string;
/**
* indicates if action should be visible for given invocation context.
* It also can be a simple boolean value.
* `True` by default.
* The most common example of usage is to hide resources from the UI.
* So let say we have 2 resources __User__ and __Cars__:
*
* ```javascript
* const User = mongoose.model('User', mongoose.Schema({
* email: String,
* encryptedPassword: String,
* }))
* const Car = mongoose.model('Car', mongoose.Schema({
* name: String,
* ownerId: { type: mongoose.Types.ObjectId, ref: 'User' },
* })
* ```
*
* so if we want to hide Users collection, but allow people to pick user when
* creating cars. We can do this like this:
*
* ```javascript
* new AdminJS({ resources: [{
* resource: User,
* options: { actions: { list: { isVisible: false } } }
* }]})
* ```
* In contrast - when we use {@link Action#isAccessible} instead - user wont be able to
* pick car owner.
*
* @see {@link ActionContext} parameter passed to isAccessible
* @see {@link IsFunction} exact type of the function
*/
isVisible?: boolean | IsFunction;
/**
* Indicates if the action can be invoked for given invocation context.
* You can pass a boolean or function of type {@link IsFunction}, which
* takes {@link ActionContext} as an argument.
*
* You can use it as a carrier between the hooks.
*
* Example for isVisible function which allows the user to edit cars which belongs only
* to her:
*
* ```javascript
* const canEditCars = ({ currentAdmin, record }) => {
* return currentAdmin && (
* currentAdmin.role === 'admin'
* || currentAdmin._id === record.param('ownerId')
* )
* }
*
* new AdminJS({ resources: [{
* resource: Car,
* options: { actions: { edit: { isAccessible: canEditCars } } }
* }]})
* ```
*
* @see {@link ActionContext} parameter passed to isAccessible
* @see {@link IsFunction} exact type of the function
*/
isAccessible?: boolean | IsFunction;
/**
* If filter should be visible on the sidebar. Only for _resource_ actions
*
* Example of creating new resource action with filter
*
* ```javascript
* new AdminJS({ resources: [{
* resource: Car,
* options: { actions: {
* newAction: {
* type: 'resource',
* showFilter: true,
* }
* }}
* }]})
* ```
*/
showFilter?: boolean;
/**
* If action should have resource actions buttons displayed above action header.
*
* Defaults to `true`
*
* @new in version v5.8.1
*/
showResourceActions?: boolean;
/**
* Type of an action - could be either _resource_, _record_ or _bulk_.
*
* <img src="./images/actions.png">
*
* When you define a new action - it is required.
*/
actionType: ActionType;
/**
* icon name for the action. Take a look {@link Icon} component,
* because what you put here is passed down to it.
*
* ```javascript
* new AdminJS({ resources: [{
* resource: Car,
* options: { actions: { edit: { icon: 'Add' } } },
* }]})
* ```
*/
icon?: IconProps['icon'];
/**
* guard message - user will have to confirm it before executing an action.
*
* ```javascript
* new AdminJS({ resources: [{
* resource: Car,
* options: { actions: {
* delete: {
* guard: 'doYouReallyWantToDoThis',
* }
* }}
* }]})
* ```
*
* What you enter there goes to a translate function,
* so in order to define the actual message you will have to specify its
* translation in {@link AdminJSOptions.Locale}
*/
guard?: string;
/**
* Component which will be used to render the action. To pass the component
* use {@link ComponentLoader.add} or {@link ComponentLoader.override} method.
*
* Action components accepts {@link ActionProps} and are rendered by the
* {@link BaseActionComponent}
*
* When component is set to `false` then action doesn't have it's own view.
* Instead after clicking button it is immediately performed. Example of
* an action without a view is {@link module:DeleteAction}.
*/
component?: string | false;
/**
* handler function which will be invoked by either:
* - {@link ApiController#resourceAction}
* - {@link ApiController#recordAction}
* - or {@link ApiController#bulkAction}
* when user visits clicks action link.
*
* If you are defining this action for a record it has to return:
* - {@link ActionResponse} for resource action
* - {@link RecordActionResponse} for record action
* - {@link BulkActionResponse} for bulk action
*
* ```javascript
* // Handler of a 'record' action
* handler: async (request, response, context) {
* const user = context.record
* const Cars = context._admin.findResource('Car')
* const userCar = Car.findOne(context.record.param('carId'))
* return {
* record: user.toJSON(context.currentAdmin),
* }
* }
* ```
*
* Required for new actions. For modifying already defined actions
* like new and edit we suggest using {@link Action#before} and {@link Action#after} hooks.
*/
handler: ActionHandler<T> | Array<ActionHandler<T>> | null;
/**
* Before action hook. When it is given - it is performed before the {@link Action#handler}
* method.
*
* Example of hashing password before creating it:
*
* ```javascript
* actions: {
* new: {
* before: async (request) => {
* if(request.payload.password) {
* request.payload = {
* ...request.payload,
* encryptedPassword: await bcrypt.hash(request.payload.password, 10),
* password: undefined,
* }
* }
* return request
* },
* }
* }
* ```
*/
before?: Before | Array<Before>;
/**
* After action hook. When it is given - it is performed on the returned,
* by {@link Action#handler handler} function response.
*
* You can use it to (just an idea)
* - create log of changes done in the app
* - prefetch additional data after original {@link Handler} is being performed
*
* Creating a changelog example:
*
* ```javascript
* // example mongoose model
* const ChangeLog = mongoose.model('ChangeLog', mongoose.Schema({
* // what action
* action: { type: String },
* // who
* userId: { type: mongoose.Types.ObjectId, ref: 'User' },
* // on which resource
* resource: { type: String },
* // was record involved (resource and recordId creates to polymorphic relation)
* recordId: { type: mongoose.Types.ObjectId },
* }, { timestamps: true }))
*
* // actual after function
* const createLog = async (originalResponse, request, context) => {
* // checking if object doesn't have any errors or is a delete action
* if ((request.method === 'post'
* && originalResponse.record
* && !Object.keys(originalResponse.record.errors).length)
* || context.action.name === 'delete') {
* await ChangeLog.create({
* action: context.action.name,
* // assuming in the session we store _id of the current admin
* userId: context.currentAdmin && context.currentAdmin._id,
* resource: context.resource.id(),
* recordId: context.record && context.record.id(),
* })
* }
* return originalResponse
* }
*
* // and attaching this function to actions for all resources
* const { ACTIONS } = require('adminjs')
*
* ACTIONS.edit.after = [createLog]
* ACTIONS.delete.after = [createLog]
* ACTIONS.new.after = [createLog]
* ```
*
*/
after?: After<T> | Array<After<T>>;
/**
* Indicates if given action should be seen in a drawer or in a full screen. Default to false
*/
showInDrawer?: boolean;
/**
* Indicates if Action Header should be hidden.
* Action header consist of:
* - breadcrumbs
* - action buttons
* - action title
*/
hideActionHeader?: boolean;
/**
* The max width of action HTML container.
* You can put here an actual size in px or an array of widths, where different values
* will be responsible for different breakpoints.
* It is directly passed to action's wrapping {@link Box} component, to its `width` property.
*
* Examples
* ```javascript
*
* // passing regular string
* containerWidth: '800px'
*
* // passing number for 100% width
* containerWidth: 1
*
* // passing values for different {@link breakpoints}
* containerWidth: [1, 1/2, 1/3]
* ```
*/
containerWidth?: string | number | Array<string | number>;
/**
* Definition for the layout. Works with the edit and show actions.
*
* With the help of {@link LayoutElement} you can put all the properties to whatever
* layout you like, without knowing React.
*
* This is an example of defining a layout
*
* ```
* const layout = [{ width: 1 / 2 }, [
* ['@H3', { children: 'Company data' }],
* 'companyName',
* 'companySize',
* ]],
* [
* ['@H3', { children: 'Contact Info' }],
* [{ flexDirection: 'row', flex: true }, [
* ['email', { pr: 'default', flexGrow: 1 }],
* ['address', { flexGrow: 1 }],
* ]],
* ],
* ]
* ```
*
* Alternatively you can pass a {@link LayoutElementFunction function} taking
* {@link CurrentAdmin} as an argument. This will allow you to show/hide
* given property for restricted users.
*
* To see entire documentation and more examples visit {@link LayoutElement}
*
* @see LayoutElement
* @see LayoutElementFunction
*/
layout?: LayoutElementFunction | Array<LayoutElement>;
/**
* Defines the variant of the action. based on that it will receive given color.
* @new in version v3.3
*/
variant?: VariantType;
/**
* Action can be nested. If you give here another action name - it will be nested under it.
* If parent action doesn't exists - it will be nested under name in the parent.
* @new in version v3.3
*/
parent?: string;
/**
* Any custom properties you want to pass down to {@link ActionJSON}. They have to
* be stringified.
* @new in version v3.3
*/
custom?: Record<string, any>;
}
================================================
FILE: src/backend/actions/bulk-delete/bulk-delete-action.spec.ts
================================================
import chai, { expect } from 'chai'
import chaiAsPromised from 'chai-as-promised'
import sinon from 'sinon'
import BulkDeleteAction from './bulk-delete-action.js'
import { ActionContext, ActionRequest, ActionHandler, BulkActionResponse } from '../action.interface.js'
import BaseRecord from '../../adapters/record/base-record.js'
import AdminJS from '../../../adminjs.js'
import ViewHelpers from '../../utils/view-helpers/view-helpers.js'
import BaseResource from '../../adapters/resource/base-resource.js'
import ActionDecorator from '../../decorators/action/action-decorator.js'
import NotFoundError from '../../utils/errors/not-found-error.js'
import { RecordJSON } from '../../../frontend/interfaces/index.js'
import { CurrentAdmin } from '../../../current-admin.interface.js'
chai.use(chaiAsPromised)
describe('BulkDeleteAction', function () {
let data: ActionContext
const request = {} as ActionRequest
let response: any
describe('.handler', function () {
afterEach(function () {
sinon.restore()
})
beforeEach(async function () {
data = {
_admin: sinon.createStubInstance(AdminJS),
translateMessage: sinon.stub<any, string>().returns('translatedMessage'),
h: sinon.createStubInstance(ViewHelpers),
resource: sinon.createStubInstance(BaseResource),
action: sinon.createStubInstance(ActionDecorator) as unknown as ActionDecorator,
} as unknown as ActionContext
})
it('throws error when no records are given', async function () {
await expect(
(BulkDeleteAction.handler as ActionHandler<BulkActionResponse>)(request, response, data),
).to.rejectedWith(NotFoundError)
})
context('2 records were selected', function () {
let record: BaseRecord
let recordJSON: RecordJSON
beforeEach(function () {
recordJSON = { id: 'someId' } as RecordJSON
record = sinon.createStubInstance(BaseRecord, {
toJSON: sinon.stub<[(CurrentAdmin)?]>().returns(recordJSON),
}) as unknown as BaseRecord
data.records = [record]
})
it('returns all records for get request', async function () {
request.method = 'get'
await expect(
(BulkDeleteAction.handler as ActionHandler<BulkActionResponse>)(request, response, data),
).to.eventually.deep.equal({
records: [recordJSON],
})
})
it('deletes all records for post request', async function () {
request.method = 'post'
await (
BulkDeleteAction.handler as ActionHandler<BulkActionResponse>
)(request, response, data)
expect(data.resource.delete).to.have.been.calledOnce
})
it('returns deleted records, notice and redirectUrl for post request', async function () {
request.method = 'post'
const actionResponse = await (
BulkDeleteAction.handler as ActionHandler<BulkActionResponse>
)(request, response, data)
expect(actionResponse).to.have.property('notice')
expect(actionResponse).to.have.property('redirectUrl')
expect(actionResponse).to.have.property('records')
})
})
})
})
================================================
FILE: src/backend/actions/bulk-delete/bulk-delete-action.ts
================================================
import { Action, BulkActionResponse } from '../action.interface.js'
import NotFoundError from '../../utils/errors/not-found-error.js'
/**
* @implements Action
* @category Actions
* @module BulkDeleteAction
* @description
* Removes given records from the database.
* @private
*/
export const BulkDeleteAction: Action<BulkActionResponse> = {
name: 'bulkDelete',
isVisible: true,
actionType: 'bulk',
icon: 'Trash2',
showInDrawer: true,
variant: 'danger',
/**
* Responsible for deleting existing records.
*
* To invoke this action use {@link ApiClient#bulkAction}
* with {actionName: _bulkDelete_}
*
* @return {Promise<BulkActionResponse>}
* @implements ActionHandler
* @memberof module:BulkDeleteAction
*/
handler: async (request, response, context) => {
const { records, resource, h } = context
if (!records || !records.length) {
throw new NotFoundError('no records were selected.', 'Action#handler')
}
if (request.method === 'get') {
const recordsInJSON = records.map((record) => record.toJSON(context.currentAdmin))
return {
records: recordsInJSON,
}
}
if (request.method === 'post') {
await Promise.all(records.map((record) => resource.delete(record.id(), context)))
return {
records: records.map((record) => record.toJSON(context.currentAdmin)),
notice: {
message: records.length > 1 ? 'successfullyBulkDeleted_plural' : 'successfullyBulkDeleted',
options: { count: records.length },
resourceId: resource.id(),
type: 'success',
},
redirectUrl: h.resourceUrl({ resourceId: resource._decorated?.id() || resource.id() }),
}
}
throw new Error('method should be either "post" or "get"')
},
}
export default BulkDeleteAction
================================================
FILE: src/backend/actions/delete/delete-action.spec.ts
================================================
import chai, { expect } from 'chai'
import chaiAsPromised from 'chai-as-promised'
import sinon from 'sinon'
import DeleteAction from './delete-action.js'
import { ActionContext, ActionRequest, ActionHandler, RecordActionResponse } from '../action.interface.js'
import BaseRecord from '../../adapters/record/base-record.js'
import AdminJS from '../../../adminjs.js'
import ViewHelpers from '../../utils/view-helpers/view-helpers.js'
import BaseResource from '../../adapters/resource/base-resource.js'
import ActionDecorator from '../../decorators/action/action-decorator.js'
import NotFoundError from '../../utils/errors/not-found-error.js'
import { ValidationError } from '../../utils/errors/validation-error.js'
import { RecordJSON } from '../../../frontend/interfaces/index.js'
import { CurrentAdmin } from '../../../current-admin.interface.js'
chai.use(chaiAsPromised)
describe('DeleteAction', function () {
let data: ActionContext
const request = {
params: {},
method: 'post',
} as ActionRequest
let response: any
describe('.handler', function () {
afterEach(function () {
sinon.restore()
})
beforeEach(async function () {
data = {
_admin: sinon.createStubInstance(AdminJS),
h: sinon.createStubInstance(ViewHelpers),
resource: sinon.createStubInstance(BaseResource),
action: sinon.createStubInstance(ActionDecorator) as unknown as ActionDecorator,
} as unknown as ActionContext
})
it('throws error when no records are given', async function () {
await expect(
(DeleteAction.handler as ActionHandler<RecordActionResponse>)(request, response, data),
).to.rejectedWith(NotFoundError)
})
context('A record has been selected', function () {
let record: BaseRecord
let recordJSON: RecordJSON
beforeEach(function () {
recordJSON = { id: 'someId' } as RecordJSON
record = sinon.createStubInstance(BaseRecord, {
toJSON: sinon.stub<[(CurrentAdmin)?]>().returns(recordJSON),
}) as unknown as BaseRecord
request.params.recordId = recordJSON.id
data.record = record
})
it('returns deleted record, notice and redirectUrl', async function () {
const actionResponse = await (
DeleteAction.handler as ActionHandler<RecordActionResponse>
)(request, response, data)
expect(actionResponse).to.have.property('notice')
expect(actionResponse).to.have.property('redirectUrl')
expect(actionResponse).to.have.property('record')
})
context('ValidationError is thrown by Resource.delete', function () {
it('returns error notice', async function () {
const errorMessage = 'test validation error'
data.resource = sinon.createStubInstance(BaseResource, {
delete: sinon.stub().rejects(new ValidationError({}, { message: errorMessage })) as any,
})
const actionResponse = await (
DeleteAction.handler as ActionHandler<RecordActionResponse>
)(request, response, data)
expect(actionResponse).to.have.property('notice')
expect(actionResponse.notice).to.deep.equal({
message: errorMessage,
type: 'error',
})
expect(actionResponse).to.have.property('record')
})
it('returns error notice with default message when ValidationError has no baseError', async function () {
data.resource = sinon.createStubInstance(BaseResource, {
delete: sinon.stub().rejects(new ValidationError({})) as any,
})
const actionResponse = await (
DeleteAction.handler as ActionHandler<RecordActionResponse>
)(request, response, data)
expect(actionResponse).to.have.property('notice')
expect(actionResponse.notice).to.deep.equal({
message: 'thereWereValidationErrors',
type: 'error',
})
expect(actionResponse).to.have.property('record')
})
})
})
})
})
================================================
FILE: src/backend/actions/delete/delete-action.ts
================================================
import { Action, RecordActionResponse } from '../action.interface.js'
import NotFoundError from '../../utils/errors/not-found-error.js'
import ValidationError from '../../utils/errors/validation-error.js'
/**
* @implements Action
* @category Actions
* @module DeleteAction
* @description
* Removes given record from the database. Since it doesn't have a
* component - it redirects right away after clicking its {@link ActionButton}
* @private
*/
export const DeleteAction: Action<RecordActionResponse> = {
name: 'delete',
isVisible: true,
actionType: 'record',
icon: 'Trash2',
guard: 'confirmDelete',
component: false,
variant: 'danger',
/**
* Responsible for deleting existing record.
*
* To invoke this action use {@link ApiClient#recordAction}
*
* @return {Promise<RecordActionResponse>}
* @implements ActionHandler
* @memberof module:DeleteAction
*/
handler: async (request, _response, context) => {
const { record, resource, currentAdmin, h } = context
if (!request.params.recordId || !record) {
throw new NotFoundError([
'You have to pass "recordId" to Delete Action',
].join('\n'), 'Action#handler')
}
if (request.method === 'get') {
return {
record: record.toJSON(context.currentAdmin),
}
}
try {
await resource.delete(request.params.recordId, context)
} catch (error) {
if (error instanceof ValidationError) {
const baseMessage = error.baseError?.message
|| 'thereWereValidationErrors'
return {
record: record.toJSON(currentAdmin),
notice: {
message: baseMessage,
type: 'error',
},
}
}
throw error
}
return {
record: record.toJSON(currentAdmin),
redirectUrl: h.resourceUrl({ resourceId: resource._decorated?.id() || resource.id() }),
notice: {
message: 'successfullyDeleted',
type: 'success',
},
}
},
}
export default DeleteAction
================================================
FILE: src/backend/actions/edit/edit-action.ts
================================================
import { Action, RecordActionResponse } from '../action.interface.js'
import NotFoundError from '../../utils/errors/not-found-error.js'
import populator from '../../utils/populator/populator.js'
import { paramConverter } from '../../../utils/param-converter/index.js'
/**
* @implements Action
* @category Actions
* @module EditAction
* @description
* Shows form for updating existing record
* @private
*
* @classdesc
* Uses {@link EditAction} component to render form
*/
export const EditAction: Action<RecordActionResponse> = {
name: 'edit',
isVisible: true,
actionType: 'record',
icon: 'Edit',
showInDrawer: false,
/**
* Responsible for updating existing record.
*
* To invoke this action use {@link ApiClient#recordAction}
*
* @return {RecordActionResponse} populated record
* @implements Action#handler
* @memberof module:EditAction
*/
handler: async (request, response, context) => {
const { record, resource, currentAdmin, h } = context
if (!record) {
throw new NotFoundError([
`Record of given id ("${request.params.recordId}") could not be found`,
].join('\n'), 'Action#handler')
}
if (request.method === 'get') {
return { record: record.toJSON(currentAdmin) }
}
const params = paramConverter.prepareParams(request.payload ?? {}, resource)
const newRecord = await record.update(params, context)
const [populatedRecord] = await populator([newRecord], context)
// eslint-disable-next-line no-param-reassign
context.record = populatedRecord
if (record.isValid()) {
return {
redirectUrl: h.resourceUrl({ resourceId: resource._decorated?.id() || resource.id() }),
notice: {
message: 'successfullyUpdated',
type: 'success',
},
record: populatedRecord.toJSON(currentAdmin),
}
}
const baseMessage = populatedRecord.baseError?.message
|| 'thereWereValidationErrors'
return {
record: populatedRecord.toJSON(currentAdmin),
notice: {
message: baseMessage,
type: 'error',
},
}
},
}
export default EditAction
================================================
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/actions/list/list-action.ts
================================================
import { flat } from '../../../utils/flat/index.js'
import { Action, ActionQueryParameters, ActionResponse } from '../action.interface.js'
import sortSetter from '../../services/sort-setter/sort-setter.js'
import Filter from '../../utils/filter/filter.js'
import populator from '../../utils/populator/populator.js'
import { RecordJSON } from '../../../frontend/interfaces/index.js'
const PER_PAGE_LIMIT = 500
/**
* @implements Action
* @category Actions
* @module ListAction
* @description
* Returns selected Records in a list form
* @private
*/
export const ListAction: Action<ListActionResponse> = {
name: 'list',
isVisible: true,
actionType: 'resource',
showFilter: true,
showInDrawer: false,
/**
* Responsible for returning data for all records.
*
* To invoke this action use {@link ApiClient#recordAction}
*
* @implements Action#handler
* @memberof module:ListAction
* @return {Promise<ListActionResponse>} records with metadata
*/
handler: async (request, response, context) => {
const { query } = request
const { sortBy, direction, filters = {} } = flat.unflatten(query || {}) as ActionQueryParameters
const { resource, _admin } = context
let { page, perPage } = flat.unflatten(query || {}) as ActionQueryParameters
if (perPage) {
perPage = +perPage > PER_PAGE_LIMIT ? PER_PAGE_LIMIT : +perPage
} else {
perPage = _admin.options.settings?.defaultPerPage ?? 10
}
page = Number(page) || 1
const listProperties = resource.decorate().getListProperties()
const firstProperty = listProperties.find((p) => p.isSortable())
let sort
if (firstProperty) {
sort = sortSetter(
{ sortBy, direction },
firstProperty.name(),
resource.decorate().options,
)
}
const filter = await new Filter(filters, resource).populate(context)
const { currentAdmin } = context
const records = await resource.find(filter, {
limit: perPage,
offset: (page - 1) * perPage,
sort,
}, context)
const populatedRecords = await populator(records, context)
// eslint-disable-next-line no-param-reassign
context.records = populatedRecords
const total = await resource.count(filter, context)
return {
meta: {
total,
perPage,
page,
direction: sort?.direction,
sortBy: sort?.sortBy,
},
records: populatedRecords.map((r) => r.toJSON(currentAdmin)),
}
},
}
export default ListAction
/**
* Response returned by List action
* @memberof module:ListAction
* @alias ListAction
*/
export type ListActionResponse = ActionResponse & {
/**
* Paginated collection of records
*/
records: Array<RecordJSON>;
/**
* Pagination metadata
*/
meta: {
page: number;
perPage: number;
direction: 'asc' | 'desc';
sortBy: string;
total: number;
};
}
================================================
FILE: src/backend/actions/new/new-action.ts
================================================
import { populator } from '../../utils/populator/index.js'
import { paramConverter } from '../../../utils/param-converter/index.js'
import { Action, RecordActionResponse } from '../action.interface.js'
/**
* @implements Action
* @category Actions
* @module NewAction
* @description
* Shows form for creating a new record
* Uses {@link NewAction} component to render form
* @private
*/
export const NewAction: Action<RecordActionResponse> = {
name: 'new',
isVisible: true,
actionType: 'resource',
icon: 'Plus',
showInDrawer: false,
variant: 'primary',
/**
* Responsible for creating new record.
*
* To invoke this action use {@link ApiClient#resourceAction}
*
* @implements Action#handler
* @memberof module:NewAction
* @return {Promise<RecordActionResponse>} populated records
*/
handler: async (request, response, context) => {
const { resource, h, currentAdmin } = context
if (request.method === 'post') {
const params = paramConverter.prepareParams(request.payload ?? {}, resource)
let record = await resource.build(params)
record = await record.create(context)
const [populatedRecord] = await populator([record], context)
// eslint-disable-next-line no-param-reassign
context.record = populatedRecord
if (record.isValid()) {
return {
redirectUrl: h.resourceUrl({ resourceId: resource._decorated?.id() || resource.id() }),
notice: {
message: 'successfullyCreated',
type: 'success',
},
record: record.toJSON(currentAdmin),
}
}
const baseMessage = populatedRecord.baseError?.message
|| 'thereWereValidationErrors'
return {
record: record.toJSON(currentAdmin),
notice: {
message: baseMessage,
type: 'error',
},
}
}
// TODO: add wrong implementation error
throw new Error('new action can be invoked only via `post` http method')
},
}
export default NewAction
================================================
FILE: src/backend/actions/search/search-action.ts
================================================
import { flat } from '../../../utils/flat/index.js'
import { Action, ActionResponse, ActionQueryParameters } from '../action.interface.js'
import { RecordJSON } from '../../../frontend/interfaces/index.js'
import Filter from '../../utils/filter/filter.js'
/**
* @implements Action
* @category Actions
* @module SearchAction
* @description
* Used to search particular record based on "title" property. It is used by
* select fields with autocomplete.
* Uses {@link ShowAction} component to render form
* @private
*/
export const SearchAction: Action<SearchActionResponse> = {
name: 'search',
isVisible: false,
actionType: 'resource',
/**
* Search records by query string.
*
* To invoke this action use {@link ApiClient#resourceAction}
* @memberof module:SearchAction
*
* @return {Promise<SearchResponse>} populated record
* @implements ActionHandler
*/
handler: async (request, response, context) => {
const { currentAdmin, resource } = context
const { query } = request
const decorated = resource.decorate()
const titlePropertyName = request.query?.searchProperty ?? decorated.titleProperty().name()
const {
sortBy = decorated.options?.sort?.sortBy || titlePropertyName,
direction = decorated.options?.sort?.direction || 'asc',
filters: customFilters = {},
perPage = 50,
page = 1,
} = flat.unflatten(query || {}) as ActionQueryParameters
const queryString = request.params && request.params.query
const queryFilter = queryString ? { [titlePropertyName]: queryString } : {}
const filters = {
...customFilters,
...queryFilter,
}
const filter = new Filter(filters, resource)
const records = await resource.find(filter, {
limit: perPage,
offset: (page - 1) * perPage,
sort: {
sortBy,
direction,
},
}, context)
return {
records: records.map((record) => record.toJSON(currentAdmin)),
}
},
}
export default SearchAction
/**
* Response of a [Search]{@link ApiController#search} action in the API
* @memberof module:SearchAction
* @alias SearchResponse
*/
export type SearchActionResponse = ActionResponse & {
/**
* List of records
*/
records: Array<RecordJSON>;
}
================================================
FILE: src/backend/actions/show/show-action.ts
================================================
import { Action, RecordActionResponse } from '../action.interface.js'
import NotFoundError from '../../utils/errors/not-found-error.js'
/**
* @implements Action
* @category Actions
* @module ShowAction
* @description
* Returns selected Record
* Uses {@link ShowAction} component to render form
* @private
*/
export const ShowAction: Action<RecordActionResponse> = {
name: 'show',
isVisible: true,
actionType: 'record',
icon: 'Monitor',
showInDrawer: false,
/**
* Responsible for returning data for given record.
*
* To invoke this action use {@link ApiClient#recordAction}
* @memberof module:ShowAction
*
* @return {Promise<RecordActionResponse>} populated record
* @implements ActionHandler
*/
handler: async (request, response, data) => {
if (!data.record) {
throw new NotFoundError([
`Record of given id ("${request.params.recordId}") could not be found`,
].join('\n'), 'Action#handler')
}
return {
record: data.record.toJSON(data.currentAdmin),
}
},
}
export default ShowAction
================================================
FILE: src/backend/adapters/database/base-database.ts
================================================
/* eslint-disable no-useless-constructor */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint class-methods-use-this: 0 no-unused-vars: 0 */
import BaseResource from '../resource/base-resource.js'
import NotImplementedError from '../../utils/errors/not-implemented-error.js'
/**
* Representation of an ORM database in AdminJS
* @category Base
*
* @mermaid
* graph LR
* A[BaseDatabase] -->|has many| B(BaseResource)
* B --> |has many|C(BaseRecord)
* B --> |has many|D(BaseProperty)
*/
class BaseDatabase {
constructor(database: any) {}
/**
* Checks if given adapter supports database provided by user
*
* @param {any} database database provided in AdminJSOptions#databases array
* @return {Boolean} if given adapter supports this database - returns true
*/
static isAdapterFor(database: any): boolean {
throw new NotImplementedError('BaseDatabase.isAdapterFor')
}
/**
* returns array of all resources (collections/tables) in the database
*
* @return {BaseResource[]}
*/
resources(): Array<BaseResource> {
throw new NotImplementedError('BaseDatabase#resources')
}
}
export default BaseDatabase
================================================
FILE: src/backend/adapters/database/index.ts
================================================
export { default as BaseDatabase } from './base-database.js'
================================================
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/property/base-property.ts
================================================
/* eslint class-methods-use-this: 0 object-curly-newline: 0 */
/**
* @name PropertyType
* @typedef {object} PropertyType
* @memberof BaseProperty
* @alias PropertyType
* @property {string} string default property type
* @property {string} float type of floating point numbers
* @property {string} number regular number
* @property {string} boolean boolean value
* @property {string} date date
* @property {string} datetime date with time
* @property {string} mixed type representing an object
* @property {string} reference many to one reference
* @property {string} richtext wysiwig editor
* @property {string} textarea resizable textarea input
* @property {string} password password field
*/
// Spacer
const TITLE_COLUMN_NAMES = ['title', 'name', 'subject', 'email']
export type PropertyType =
'string' | 'float' | 'number' | 'boolean' |
'date' | 'datetime' | 'mixed' | 'reference' | 'key-value' |
'richtext' | 'textarea' | 'password' | 'currency' | 'phone' | 'uuid';
// description
type BasePropertyAttrs = {
path: string;
type?: PropertyType;
isId?: boolean;
isSortable?: boolean;
position?: number;
}
/**
* Represents Resource Property
* @category Base
*/
class BaseProperty {
private _path: string
private _type: PropertyType
private _isId: boolean
private _isSortable: boolean
private _position: number
/**
* @param {object} options
* @param {string} options.path property path: usually it its key but when
* property is for an object the path can be
* divided to parts by dots: i.e.
* 'address.street'
* @param {PropertyType} [options.type='string']
* @param {boolean} [options.isId=false] true when field should be treated as an ID
* @param {boolean} [options.isSortable=true] if property should be sortable
*/
constructor({
path,
type = 'string',
isId = false,
isSortable = true,
position = 1,
}: BasePropertyAttrs) {
this._path = path
this._type = type
this._isId = isId
if (!this._path) {
throw new Error('you have to give path parameter when creating BaseProperty')
}
this._isSortable = isSortable
this._position = position
}
/**
* Name of the property
* @return {string} name of the property
*/
name(): string {
return this._path
}
path(): string {
return this.name()
}
position(): number {
return this._position === undefined ? 1 : this._position
}
/**
* Return type of a property
* @return {PropertyType}
*/
type(): PropertyType {
return this._type || 'string'
}
/**
* Return true if given property should be treated as a Record Title.
*
* @return {boolean}
*/
isTitle(): boolean {
return TITLE_COLUMN_NAMES.includes(this._path.toLowerCase())
}
/**
* Indicates if given property should be visible
*
* @return {Boolean}
*/
isVisible(): boolean {
return !this._path || !this._path.match('password')
}
/**
* Indicates if value of given property can be updated
*
* @return {boolean}
*/
isEditable(): boolean {
return true
}
/**
* Returns true if given property is a uniq key in a table/collection
*
* @return {boolean}
*/
isId(): boolean {
return !!this._isId
}
/**
* If property is a reference to a record of different resource
* it should contain {@link BaseResource.id} of this resource.
*
* When property is responsible for the field: 'user_id' in SQL database
* reference should be the name of the Resource which it refers to: `Users`
*/
reference(): string | null {
return null
}
/**
* Returns all available values which field can accept. It is used in case of
* enums
*
* @return {Array<String> | null} array of all available values or null when field
* is not an enum.
*/
availableValues(): Array<string> | null {
return null
}
/**
* Returns true when given property is an array
*
* @return {boolean}
*/
isArray(): boolean {
return false
}
/**
* Returns true when given property has draggable elements.
* Only usable for array properties.
*
* @return {boolean}
*/
isDraggable(): boolean {
return false
}
/**
* In case of `mixed` type returns all nested properties.
*
* @return {Array<BaseProperty>} sub properties
*/
subProperties(): Array<BaseProperty> {
return []
}
/**
* Indicates if given property can be sorted
*
* @return {boolean}
*/
isSortable(): boolean {
return this._isSortable
}
/**
* Indicates if given property is required
*/
isRequired(): boolean {
return false
}
}
export default BaseProperty
================================================
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/base-record.spec.ts
================================================
import chai, { expect } from 'chai'
import chaiChange from 'chai-change'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import chaiAsPromised from 'chai-as-promised'
import { ParamsType } from './params.type.js'
import BaseRecord from './base-record.js'
import BaseResource from '../resource/base-resource.js'
import BaseProperty from '../property/base-property.js'
import ValidationError, { PropertyErrors } from '../../utils/errors/validation-error.js'
import RecordError from '../../utils/errors/record-error.js'
import { ActionDecorator, ResourceDecorator } from '../../decorators/index.js'
chai.use(chaiAsPromised)
chai.use(chaiChange)
chai.use(sinonChai)
describe('Record', function () {
let record: BaseRecord
let params: BaseRecord['params'] = { param1: 'john' }
afterEach(function () {
sinon.restore()
})
describe('#get', function () {
context('record with nested parameters', function () {
const nested3level = 'value'
beforeEach(function () {
params = {
nested1level: { nested2level: { nested3level } },
}
record = new BaseRecord(params, {} as BaseResource)
})
it('returns deepest field when all up-level keys are given', function () {
expect(record.get('nested1level.nested2level.nested3level')).to.equal(nested3level)
})
it('returns object when all up-level keys are given except one', function () {
expect(record.get('nested1level.nested2level')).to.deep.equal({ nested3level })
})
it('returns object when only first level key is given', function () {
expect(record.get('nested1level')).to.deep.equal({
nested2level: { nested3level },
})
})
it('returns undefined when passing unknown param', function () {
expect(record.get('nested1level.nested2')).to.be.undefined
})
})
})
describe('#constructor', function () {
it('returns empty object if params are not passed to the constructor', function () {
record = new BaseRecord({}, {} as BaseResource)
expect((record as any).params).to.deep.equal({})
})
it('stores flatten object params', function () {
record = new BaseRecord({ auth: { login: 'login' } }, {} as BaseResource)
expect((record as any).params).to.deep.equal({ 'auth.login': 'login' })
})
})
describe('#save', function () {
const newParams = { param2: 'doe' }
const properties = [new BaseProperty({ path: '_id', isId: true })]
let resource: BaseResource
beforeEach(function () {
resource = sinon.createStubInstance(BaseResource, {
properties: sinon.stub<[], BaseProperty[]>().returns(properties),
create: sinon.stub<[Record<string, any>], Promise<ParamsType>>()
.resolves(newParams),
update: sinon.stub<[string, Record<string, any>], Promise<ParamsType>>()
.resolves(newParams),
})
})
it('uses BaseResource#create method when there is no id property', async function () {
record = new BaseRecord(newParams, resource)
record.save()
expect(resource.create).to.have.been.calledWith(newParams)
})
it('uses BaseResource#update method when there is a id property', function () {
const _id = '1231231313'
record = new BaseRecord({ ...newParams, _id }, resource)
record.save()
expect(resource.update).to.have.been.calledWith(_id, { ...newParams, _id })
})
it('stores validation error when they happen', async function () {
const baseError: RecordError = {
message: 'test base error',
}
const propertyErrors: PropertyErrors = {
param2: {
type: 'required',
message: 'Field is required',
},
}
resource.create = sinon.stub().rejects(new ValidationError(propertyErrors, baseError))
record = new BaseRecord(newParams, resource)
await record.save()
expect(record.error('param2')).to.deep.equal(propertyErrors.param2)
expect(record.baseError).to.deep.equal(baseError)
})
it('stores validation error when they happen (even when there is no baseError specified)', async function () {
const propertyErrors: PropertyErrors = {
param2: {
type: 'required',
message: 'Field is required',
},
}
resource.create = sinon.stub().rejects(new ValidationError(propertyErrors))
record = new BaseRecord(newParams, resource)
await record.save()
expect(record.error('param2')).to.deep.equal(propertyErrors.param2)
expect(record.baseError).to.be.null
})
})
describe('#update', function () {
const newParams = { param2: 'doe' }
const properties = [new BaseProperty({ path: '_id', isId: true })]
params = { param1: 'john', _id: '1381723981273' }
let resource: BaseResource
context('resource stores the value', function () {
beforeEach(async function () {
resource = sinon.createStubInstance(BaseResource, {
properties: sinon.stub<[], BaseProperty[]>().returns(properties),
update: sinon.stub<[string, Record<string, any>], Promise<ParamsType>>()
.resolves(newParams),
})
record = new BaseRecord(params, resource)
await record.update(newParams)
})
it('stores what was returned by BaseResource#update to this.params', function () {
expect(record.get('param2')).to.equal(newParams.param2)
})
it('resets the baseError when there is none', function () {
expect((record as any).baseError).to.deep.equal(null)
})
it('resets the errors when there are none', function () {
expect((record as any).errors).to.deep.equal({})
})
it('calls the BaseResource#update function with the id and new params', function () {
expect(resource.update).to.have.been.calledWith(params._id, newParams)
})
})
context('resource throws validation error', function () {
const baseError: RecordError = {
message: 'test base error',
}
const propertyErrors: PropertyErrors = {
param2: {
type: 'required',
message: 'Field is required',
},
}
beforeEach(async function () {
resource = sinon.createStubInstance(BaseResource, {
properties: sinon.stub<[], BaseProperty[]>().returns(properties),
update: sinon.stub<[string, Record<string, any>], Promise<ParamsType>>()
.rejects(new ValidationError(propertyErrors, baseError)),
})
record = new BaseRecord(params, resource)
this.returnedValue = await record.update(newParams)
})
it('stores validation baseError', function () {
expect(record.baseError).to.deep.equal(baseError)
})
it('stores validation errors', function () {
expect(record.error('param2')).to.deep.equal(propertyErrors.param2)
})
it('returns itself', function () {
expect(this.returnedValue).to.equal(record)
})
})
})
describe('#isValid', function () {
it('returns true when there are no errors', function () {
(record as any).errors = {}
expect(record.isValid()).to.equal(true)
})
it('returns false when there is at least on error', function () {
(record as any).errors = {
pathWithError: { type: 'required', message: 'I am error' },
}
expect(record.isValid()).to.equal(false)
})
})
describe('#title', function () {
const properties = [new BaseProperty({ path: 'name' })]
params = { name: 'john', _id: '1381723981273' }
it('returns value in title property', function () {
const resource = sinon.createStubInstance(BaseResource, {
properties: sinon.stub<[], BaseProperty[]>().returns(properties),
})
record = new BaseRecord(params, resource)
expect(record.title()).to.equal(params.name)
})
})
describe('#populate', function () {
it('sets populated field', function () {
const populated = { value: new BaseRecord({}, {} as BaseResource) }
record = new BaseRecord(params, {} as BaseResource)
record.populate('value', populated.value)
expect((record as any).populated.value).to.equal(populated.value)
})
it('clears populated field when record is null or undefined', () => {
record = new BaseRecord(params, {} as BaseResource)
record.populate('value', 'something' as any)
expect(() => {
record.populate('value', null)
}).to.alter(() => record.populated.value, { from: 'something', to: undefined })
})
})
describe('#toJSON', () => {
const param = 'populatedProperty'
let resource: BaseResource
beforeEach(() => {
resource = sinon.createStubInstance(BaseResource, {
properties: sinon.stub<[], BaseProperty[]>().returns([]),
decorate: sinon.stub<[], ResourceDecorator>().returns(
sinon.createStubInstance(ResourceDecorator, {
recordActions: sinon.stub<[BaseRecord], ActionDecorator[]>().returns([]),
bulkActions: sinon.stub<[BaseRecord], ActionDecorator[]>().returns([]),
}) as unknown as ResourceDecorator,
),
})
record = new BaseRecord(params, resource)
})
it('changes populated records to JSON', () => {
const refRecord = sinon.createStubInstance(BaseRecord, {
toJSON: sinon.stub(),
})
record.populate(param, refRecord)
sinon.stub(record, 'id').returns('1')
record.toJSON()
expect(refRecord.toJSON).to.have.been.calledOnce
})
it('does not changes to JSON when in populated there is something else than BaseRecord', () => {
record.populate(param, 'something else' as unknown as BaseRecord)
sinon.stub(record, 'id').returns('1')
expect(() => {
record.toJSON()
}).not.to.throw()
})
})
})
================================================
FILE: src/backend/adapters/record/base-record.ts
================================================
import { flat, GetOptions } from '../../../utils/flat/index.js'
import { ParamsType } from './params.type.js'
import BaseResource from '../resource/base-resource.js'
import ValidationError, { PropertyErrors } from '../../utils/errors/validation-error.js'
import RecordError from '../../utils/errors/record-error.js'
import { RecordJSON } from '../../../frontend/interfaces/index.js'
import { CurrentAdmin } from '../../../current-admin.interface.js'
import { ActionContext } from '../../actions/index.js'
/**
* Representation of an particular ORM/ODM Record in given Resource in AdminJS
*
* @category Base
*/
class BaseRecord {
/**
* Resource to which record belongs
*/
public resource: BaseResource
/**
* Actual record data stored as a flatten object. You shouldn't access them directly - always
* with {@link BaseRecord#get} and {@link BaseRecord#set} property.
*/
public params: ParamsType
/**
* Object containing any base/overall validation error messages:
* this.baseError = { message: 'errorMessage' }
*/
public baseError: RecordError | null
/**
* Object containing all validation errors: this.errors[path] = { message: 'errorMessage' }
*/
public errors: PropertyErrors
/**
* Object containing all populated relations.
*/
public populated: {[key: string]: BaseRecord}
/**
* @param {ParamsType} params all resource data. I.e. field values
* @param {BaseResource} resource resource to which given record belongs
*/
constructor(params: ParamsType, resource: BaseResource) {
this.resource = resource
this.params = params ? flat.flatten(params) : {}
this.baseError = null
this.errors = {}
this.populated = {}
}
/**
* Returns value for given field.
* @param {string} path path (name) for given field: i.e. 'email' or 'authentication.email'
* if email is nested within the authentication object in the data
* store
* @return {any} value for given field
* @deprecated in favour of {@link BaseRecord#get} and {@link BaseRecord#set} methods
*/
param(path: string): any {
return flat.get(this.params, path)
}
/**
* Returns unflatten (regular) value for given field. So if you have in the params following
* structure:
* ```javascript
* params = {
* genre.0: 'male',
* genre.1: 'female',
* }
* ```
*
* for `get('genre')` function will return ['male', 'female']
*
* @param {string} [propertyPath] path for the property. If not set function returns an entire
* unflatten object
* @param {GetOptions} [options]
* @return {any} unflatten data under given path
* @new in version 3.3
*/
get(propertyPath?: string, options?: GetOptions): any {
return flat.get(this.params, propertyPath, options)
}
/**
* Sets given value under the propertyPath. Value is flatten and all previous values under this
* path are replaced. When value is `undefined` function just clears the old values
*
* @param {string} propertyPath
* @param {any} value
* @returns an entire, updated, params object
* @new in version 3.3
*/
set(propertyPath: string, value: any): any {
this.params = flat.set(this.params, propertyPath, value)
return this.params
}
/**
* Returns object containing all params keys starting with prefix
*
* @param {string} prefix
*
* @return {object | undefined}
* @deprecated in favour of {@link selectParams}
*/
namespaceParams(prefix: string): Record<string, any> | void {
return flat.selectParams(this.params, prefix)
}
/**
* Returns object containing all params keys starting with prefix
*
* @param {string} prefix
* @param {GetOptions} [options]
*
* @return {object | undefined}
* @new in version 3.3
*/
selectParams(prefix: string, options?: GetOptions): Record<string, any> | void {
return flat.selectParams(this.params, prefix, options)
}
/**
* Updates given Record in the data store. Practically it invokes
* {@link BaseResource.update} method.
*
* When validation error occurs it stores that to {@link BaseResource.errors}
*
* @param {object} params all field with values which has to be updated
* @param {ActionContext} [context]
* @return {Promise<BaseRecord>} given record (this)
*/
async update(params, context?: ActionContext): Promise<BaseRecord> {
try {
this.storeParams(params)
const returnedParams = await this.resource.update(this.id(), params, context)
this.storeParams(returnedParams)
} catch (e) {
if (e instanceof ValidationError) {
this.baseError = e.baseError
this.errors = e.propertyErrors
return this
}
throw e
}
this.baseError = null
this.errors = {}
return this
}
/**
* Saves the record in the database. When record already exists - it updates, otherwise
* it creates new one.
*
* Practically it invokes
* {@link BaseResource#create} or {@link BaseResource#update} methods.
*
* When validation error occurs it stores that to {@link BaseResource#errors}
* @param {ActionContext} [context]
* @return {Promise<BaseRecord>} given record (this)
*/
async save(context?: ActionContext): Promise<BaseRecord> {
try {
let returnedParams
if (this.id()) {
returnedParams = await this.resource.update(this.id(), this.params, context)
} else {
returnedParams = await this.resource.create(this.params, context)
}
this.storeParams(returnedParams)
} catch (e) {
if (e instanceof ValidationError) {
this.baseError = e.baseError
this.errors = e.propertyErrors
return this
}
throw e
}
this.baseError = null
this.errors = {}
return this
}
/**
* Creates the record in the database
*
* Practically it invokes
* {@link BaseResource#create}.
*
* When validation error occurs it stores that to {@link BaseResource#errors}
*
*
* @return {Promise<BaseRecord>} given record (this)
* @param {ActionContext} [context]
*/
async create(context?: ActionContext): Promise<BaseRecord> {
try {
const returnedParams = await this.resource.create(this.params, context)
this.storeParams(returnedParams)
} catch (e) {
if (e instanceof ValidationError) {
this.baseError = e.baseError
this.errors = e.propertyErrors
return this
}
throw e
}
this.baseError = null
this.errors = {}
return this
}
/**
* Returns uniq id of the Record.
* @return {string | number} id of the Record
*/
id(): string {
const idProperty = this.resource.properties().find((p) => p.isId())
if (!idProperty) {
throw new Error(`Resource: "${this.resource.id()}" does not have an id property`)
}
return this.params[idProperty.name()]
}
/**
* Returns title of the record. Usually title is an value for fields like: email, topic,
* title etc.
*
* Title will be shown in the breadcrumbs for example.
*
* @return {string} title of the record
*/
title(): string {
const nameProperty = this.resource.properties().find((p) => p.isTitle())
return nameProperty ? this.param(nameProperty.name()) : this.id()
}
/**
* Return state of validation for given record
* @return {boolean} if record is valid or not.
*/
isValid(): boolean {
return Object.keys(this.errors).length === 0
}
/**
* Returns error message for given property path (name)
* @param {string} path (name) of property which we want to check if is valid
* @return {RecordError | null} validation message of null
*/
error(path: string): RecordError | null {
return this.errors[path]
}
/**
* Populate record relations
*
* @param {string} propertyPath name of the property which should be populated
* @param {BaseRecord | null} [record] record to which property relates. If record is null
* or undefined - function clears the previous value
*/
populate(propertyPath: string, record?: BaseRecord | null): void {
if (record === null || typeof record === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [propertyPath]: oldValue, ...rest } = this.populated
this.populated = rest
} else {
this.populated[propertyPath] = record
}
}
/**
* Returns JSON representation of an record
* @param {CurrentAdmin} [currentAdmin]
* @return {RecordJSON}
*/
toJSON(currentAdmin?: CurrentAdmin): RecordJSON {
const populated = Object.keys(this.populated).reduce((m, key) => {
// sometimes user can add some arbitrary element to populated object. In such case
// we should omit toJSON call.
if ((this.populated[key] as any).toJSON) {
m[key] = this.populated[key].toJSON(currentAdmin)
} else {
m[key] = this.populated[key]
}
return m
}, {})
return {
params: this.params,
populated,
baseError: this.baseError,
errors: this.errors,
id: this.id(),
title: this.resource.decorate().titleOf(this),
recordActions: this.resource.decorate().recordActions(this, currentAdmin)
.map((recordAction) => recordAction.toJSON(currentAdmin)),
bulkActions: this.resource.decorate().bulkActions(this, currentAdmin)
.map((recordAction) => recordAction.toJSON(currentAdmin)),
}
}
/**
* Stores incoming payloadData in record params
*
* @param {object} [payloadData]
*/
storeParams(payloadData?: object): void {
this.params = flat.merge(this.params, payloadData)
}
}
export default BaseRecord
================================================
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/base-resource.spec.ts
================================================
import chai, { expect } from 'chai'
import sinon from 'sinon'
import chaiAsPromised from 'chai-as-promised'
import BaseResource from './base-resource.js'
import NotImplementedError from '../../utils/errors/not-implemented-error.js'
import Filter from '../../utils/filter/filter.js'
import BaseRecord from '../record/base-record.js'
import AdminJS from '../../../adminjs.js'
import ResourceDecorator from '../../decorators/resource/resource-decorator.js'
chai.use(chaiAsPromised)
describe('BaseResource', function () {
let resource: BaseResource
beforeEach(function () {
resource = new BaseResource({})
})
afterEach(function () {
sinon.restore()
})
describe('.isAdapterFor', function () {
it('throws NotImplementedError', async function () {
expect(() => BaseResource.isAdapterFor({})).to.throw(NotImplementedError)
})
})
describe('#databaseName', function () {
it('throws NotImplementedError', async function () {
expect(() => resource.databaseName()).to.throw(NotImplementedError)
})
})
describe('#databaseType', function () {
it('returns "database" by default', async function () {
expect(resource.databaseType()).to.eq('other')
})
})
describe('#id', function () {
it('throws NotImplementedError', async function () {
expect(() => resource.id()).to.throw(NotImplementedError)
})
})
describe('#properties', function () {
it('throws NotImplementedError', async function () {
expect(() => resource.properties()).to.throw(NotImplementedError)
})
})
describe('#property', function () {
it('throws NotImplementedError', async function () {
expect(() => resource.property('someProperty')).to.throw(NotImplementedError)
})
})
describe('#count', function () {
it('throws NotImplementedError', async function () {
expect(resource.count({} as Filter)).to.be.rejectedWith(NotImplementedError)
})
})
describe('#find', function () {
it('throws NotImplementedError', async function () {
expect(resource.find({} as Filter, {})).to.be.rejectedWith(NotImplementedError)
})
})
describe('#findOne', function () {
it('throws NotImplementedError', async function () {
expect(resource.findOne('someId')).to.be.rejectedWith(NotImplementedError)
})
})
describe('#build', function () {
it('returns new BaseRecord', async function () {
const params = { param: 'value' }
expect(resource.build(params)).to.be.instanceOf(BaseRecord)
})
})
describe('#create', function () {
it('throws NotImplementedError', async function () {
expect(resource.create({})).to.be.rejectedWith(NotImplementedError)
})
})
describe('#update', function () {
it('throws NotImplementedError', async function () {
expect(resource.update('id', {})).to.be.rejectedWith(NotImplementedError)
})
})
describe('#delete', function () {
it('throws NotImplementedError', async function () {
expect(resource.delete('id')).to.be.rejectedWith(NotImplementedError)
})
})
describe('#decorate', function () {
it('returns new Decorator when resource has been decorated', function () {
sinon.stub(resource, 'properties').returns([])
resource.assignDecorator(new AdminJS(), {})
expect(resource.decorate()).to.be.instanceOf(ResourceDecorator)
})
it('throws error when resource has not been decorated', function () {
expect(() => resource.decorate()).to.throw('resource does not have any assigned decorator yet')
})
})
})
================================================
FILE: src/backend/adapters/resource/base-resource.ts
================================================
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint class-methods-use-this: 0 no-unused-vars: 0 */
/* eslint no-useless-constructor: 0 */
import { SupportedDatabasesType } from './supported-databases.type.js'
import { BaseProperty, BaseRecord, ParamsType } from '../index.js'
import { NotImplementedError, Filter } from '../../utils/index.js'
import { ResourceOptions, ResourceDecorator } from '../../decorators/index.js'
import AdminJS from '../../../adminjs.js'
import { ActionContext } from '../../actions/index.js'
/**
* Representation of a ORM Resource in AdminJS. Visually resource is a list item in the sidebar.
* Each resource has many records and many properties.
*
* Analogy is REST resource.
*
* It is an __abstract class__ and all database adapters should implement extend it implement
* following methods:
*
* - (static) {@link BaseResource.isAdapterFor isAdapterFor()}
* - {@link BaseResource#databaseName databaseName()}
* - {@link BaseResource#name name()}
* - {@link BaseResource#id id()}
* - {@link BaseResource#properties properties()}
* - {@link BaseResource#property property()}
* - {@link BaseResource#count count()}
* - {@link BaseResource#find find()}
* - {@link BaseResource#findOne findOne()}
* - {@link BaseResource#findMany findMany()}
* - {@link BaseResource#create create()}
* - {@link BaseResource#update update()}
* - {@link BaseResource#delete delete()}
* @category Base
* @abstract
* @hideconstructor
*/
class BaseResource {
public _decorated: ResourceDecorator | null
/**
* Checks if given adapter supports resource provided by the user.
* This function has to be implemented only if you want to create your custom
* database adapter.
*
* For one time Admin Resource creation - it is not needed.
*
* @param {any} rawResource resource provided in AdminJSOptions#resources array
* @return {Boolean} if given adapter supports this resource - returns true
* @abstract
*/
static isAdapterFor(rawResource): boolean {
throw new NotImplementedError('BaseResource.isAdapterFor')
}
/**
* Creates given resource based on the raw resource object
*
* @param {Object} [resource]
*/
constructor(resource?: any) {
this._decorated = null
}
/**
* The name of the database to which resource belongs. When resource is
* a mongoose model it should be database name of the mongo database.
*
* Visually, by default, all resources are nested in sidebar under their database names.
* @return {String} database name
* @abstract
*/
databaseName(): string {
throw new NotImplementedError('BaseResource#databaseName')
}
/**
* Returns type of the database. It is used to compute sidebar icon for
* given resource. Default: 'database'
* @return {String}
*/
databaseType(): SupportedDatabasesType | string {
return 'other'
}
/**
* Each resource has to have uniq id which will be put to an URL of AdminJS routes.
* For instance in {@link Router} path for the `new` form is `/resources/{resourceId}/new`
* @return {String} uniq resource id
* @abstract
*/
id(): string {
throw new NotImplementedError('BaseResource#id')
}
/**
* returns array of all properties which belongs to resource
* @return {BaseProperty[]}
* @abstract
*/
properties(): Array<BaseProperty> {
throw new NotImplementedError('BaseResource#properties')
}
/**
* returns property object for given field
* @param {String} path path/name of the property. Take a look at
* {@link BaseProperty} to learn more about
* property paths.
* @return {BaseProperty | null}
* @abstract
*/
property(path: string): BaseProperty | null {
throw new NotImplementedError('BaseResource#property')
}
/**
* Returns number of elements for given resource by including filters
* @param {Filter} filter represents what data should be included
* @param {ActionContext} [context]
* @return {Promise<Number>}
* @abstract
*/
async count(filter: Filter, context?: ActionContext): Promise<number> {
throw new NotImplementedError('BaseResource#count')
}
/**
* Returns actual records for given resource
*
* @param {Filter} filter what data should be included
* @param {Object} options
* @param {Number} [options.limit] how many records should be taken
* @param {Number} [options.offset] offset
* @param {Object} [options.sort] sort
* @param {Number} [options.sort.sortBy] sortable field
* @param {Number} [options.sort.direction] either asc or desc
* @param {ActionContext} [context]
* @return {Promise<BaseRecord[]>} list of records
* @abstract
* @example
* // filters example
* {
* name: 'Tom',
* createdAt: { from: '2019-01-01', to: '2019-01-18' }
* }
*/
async find(filter: Filter, options: {
limit?: number;
offset?: number;
sort?: {
sortBy?: string;
direction?: 'asc' | 'desc';
};
}, context?: ActionContext): Promise<Array<BaseRecord>> {
throw new NotImplementedError('BaseResource#find')
}
/**
* Finds one Record in the Resource by its id
*
* @param {String} id uniq id of the Resource Record
* @param {ActionContext} [context]
* @return {Promise<BaseRecord> | null} record
* @abstract
*/
async findOne(id: string, context?: ActionContext): Promise<BaseRecord | null> {
throw new NotImplementedError('BaseResource#findOne')
}
/**
* Finds many records based on the resource ids
*
* @param {Array<string>} ids list of ids to find
* @param {ActionContext} [context]
*
* @return {Promise<Array<BaseRecord>>} records
*/
async findMany(ids: Array<string | number>, context?: ActionContext):
Promise<Array<BaseRecord>> {
throw new NotImplementedError('BaseResource#findMany')
}
/**
* Builds new Record of given Resource.
*
* Each Record is an representation of the resource item. Before it can be saved,
* it has to be instantiated.
*
* This function has to be implemented if you want to create new records.
*
* @param {Record<string, any>} params
* @return {BaseRecord}
*/
build(params: Record<string, any>): BaseRecord {
return new BaseRecord(params, this)
}
/**
* Creates new record
*
* @param {Record<string, any>} params
* @param {ActionContext} [context]
* @return {Promise<Object>} created record converted to raw Object which
* can be used to initiate new {@link BaseRecord} instance
* @throws {ValidationError} If there are validation errors it should be thrown
* @abstract
*/
async create(params: Record<string, any>, context?: ActionContext): Promise<ParamsType> {
throw new NotImplementedError('BaseResource#create')
}
/**
* Updates the record.
*
* @param {String} id uniq id of the Resource Record
* @param {Record<string, any>} params
* @param {ActionContext} [context]
* @return {Promise<Object>} created record converted to raw Object which
* can be used to initiate new {@link BaseRecord} instance
* @throws {ValidationError} If there are validation errors it should be thrown
* @abstract
*/
async update(id: string, params: Record<string, any>, context?: ActionContext)
: Promise<ParamsType> {
throw new NotImplementedError('BaseResource#update')
}
/**
* Delete given record by id
*
* @param {String | Number} id id of the Record
* @param {ActionContext} [context]
* @throws {ValidationError} If there are validation errors it should be thrown
* @abstract
*/
async delete(id: string, context?: ActionContext): Promise<void> {
throw new NotImplementedError('BaseResource#delete')
}
/**
* Assigns given decorator to the Resource. Than it will be available under
* resource.decorate() method
*
* @param {BaseDecorator} Decorator
* @param {AdminJS} admin current instance of AdminJS
* @param {ResourceOptions} [options]
* @private
*/
assignDecorator(admin: AdminJS, options: ResourceOptions = {}): void {
this._decorated = new ResourceDecorator({ resource: this, admin, options })
}
/**
* Gets decorator object for given resource
* @return {BaseDecorator | null}
*/
decorate(): ResourceDecorator {
if (!this._decorated) {
throw new Error('resource does not have any assigned decorator yet')
}
return this._decorated
}
}
export default BaseResource
================================================
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/app.bundler.ts
================================================
import path from 'path'
import * as url from 'url'
import { InputOptions, OutputOptions } from 'rollup'
import { nodeResolve as resolve } from '@rollup/plugin-node-resolve'
import * as commonjs from '@rollup/plugin-commonjs'
import * as replace from '@rollup/plugin-replace'
import * as json from '@rollup/plugin-json'
import { minify } from 'rollup-plugin-esbuild-minify'
import { AssetBundler } from './utils/asset-bundler.js'
import { NODE_ENV } from './utils/constants.js'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const input: InputOptions = {
input: path.join(__dirname, '../../frontend/bundle-entry.js'),
external: AssetBundler.DEFAULT_EXTERNALS,
plugins: [
resolve({
extensions: AssetBundler.DEFAULT_EXTENSIONS,
mainFields: ['browser', 'main', 'module', 'jsnext:main'],
preferBuiltins: false,
}),
(json as any).default(),
(replace as any).default({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.IS_BROWSER': 'true',
'process.env.': 'AdminJS.env.',
preventAssignment: true,
'process.browser': true,
}),
(commonjs as any).default(),
...(NODE_ENV === 'production' ? [minify()] : []),
],
}
const output: OutputOptions = {
name: 'AdminJS',
file: path.join(__dirname, `../../frontend/assets/scripts/app-bundle.${NODE_ENV}.js`),
inlineDynamicImports: true,
globals: AssetBundler.DEFAULT_GLOBALS,
}
const bundler = new AssetBundler(input, output)
export default bundler
================================================
FILE: src/backend/bundler/components.bundler.ts
================================================
import { InputOptions, OutputOptions } from 'rollup'
import { nodeResolve as resolve } from '@rollup/plugin-node-resolve'
import * as commonjs from '@rollup/plugin-commonjs'
import * as replace from '@rollup/plugin-replace'
import * as json from '@rollup/plugin-json'
import { minify } from 'rollup-plugin-esbuild-minify'
import { babel } from '@rollup/plugin-babel'
import presetEnv from '@babel/preset-env'
import presetReact from '@babel/preset-react'
import presetTs from '@babel/preset-typescript'
import { AssetBundler } from './utils/asset-bundler.js'
import { COMPONENTS_ENTRY_PATH, COMPONENTS_OUTPUT_PATH, NODE_ENV } from './utils/constants.js'
const input: InputOptions = {
input: COMPONENTS_ENTRY_PATH,
external: AssetBundler.DEFAULT_EXTERNALS,
plugins: [
resolve({
extensions: AssetBundler.DEFAULT_EXTENSIONS,
mainFields: ['browser', 'main', 'module', 'jsnext:main'],
preferBuiltins: false,
}),
(json as any).default(),
(replace as any).default({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.IS_BROWSER': 'true',
'process.env.': 'AdminJS.env.',
preventAssignment: true,
'process.browser': true,
}),
(commonjs as any).default(),
babel({
extensions: AssetBundler.DEFAULT_EXTENSIONS,
babelrc: false,
babelHelpers: 'bundled',
exclude: 'node_modules/**/*.js',
presets: [
[presetEnv, {
targets: {
node: '18',
},
loose: true,
modules: false,
}],
presetReact,
presetTs,
],
plugins: ['@babel/plugin-syntax-import-assertions'],
}),
...(NODE_ENV === 'production' ? [minify()] : []),
],
}
const output: OutputOptions = {
name: 'AdminJSCustom',
file: COMPONENTS_OUTPUT_PATH,
inlineDynamicImports: true,
globals: AssetBundler.DEFAULT_GLOBALS,
}
const bundler = new AssetBundler(input, output)
export default bundler
================================================
FILE: src/backend/bundler/generate-user-component-entry.spec.js
================================================
import path from 'path'
import * as url from 'url'
import AdminJS from '../../adminjs.js'
import { ComponentLoader } from '../utils/index.js'
import generateUserComponentEntry from './generate-user-component-entry.js'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const exampleComponent = '../../../spec/fixtures/example-component.js'
const entryPath = './'
describe('generateUserComponentEntry', function () {
it('defines AdminJS.UserComponents', function () {
const adminJs = new AdminJS()
const entryFile = generateUserComponentEntry(adminJs, entryPath)
expect(entryFile).to.have.string('AdminJS.UserComponents = {}\n')
})
it('adds env variables to the entry file', function () {
const adminJs = new AdminJS({
env: { ENV_NAME: 'value' },
})
const entryFile = generateUserComponentEntry(adminJs, entryPath)
expect(entryFile).to.have.string('AdminJS.env.ENV_NAME = "value"\n')
})
it('adds components to the entry file', function () {
const loader = new ComponentLoader()
const componentId = loader.add('ExampleComponent', exampleComponent)
const adminJs = new AdminJS({ componentLoader: loader })
const rootEntryPath = path.resolve(entryPath)
const filePath = path.relative(
rootEntryPath,
path.normalize(path.join(__dirname, exampleComponent)),
)
const entryFile = generateUserComponentEntry(adminJs, entryPath)
expect(entryFile).to.have.string([
`import ${componentId} from '${filePath.replace('.js', '')}'`,
`AdminJS.UserComponents.${componentId} = ${componentId}`,
].join('\n'))
AdminJS.UserComponents = {}
})
})
================================================
FILE: src/backend/bundler/generate-user-component-entry.ts
================================================
import * as path from 'path'
import slash from 'slash'
import AdminJS from '../../adminjs.js'
/**
* Generates entry file for all UsersComponents.
* Entry consists of 3 parts:
* 1. Setup AdminJS.UserComponents map.
* 2. List of all environmental variables passed to AdminJS in configuration option.
* 3. Imports of user components defined by ComponentLoader.
*
* @param {AdminJS} admin
* @param {String} entryPath path to folder where entry file is located
* @return {String} content of an entry file
*
* @private
*/
const generateUserComponentEntry = (admin: AdminJS, entryPath: string): string => {
const { env = {} } = admin.options
admin.componentLoader.__unsafe_merge(AdminJS.__unsafe_staticComponentLoader)
const components = admin.componentLoader.getComponents()
const absoluteEntryPath = path.resolve(entryPath)
const setupPart = 'AdminJS.UserComponents = {}\n'
const envPart = Object.keys(env).map((envKey) => (
`AdminJS.env.${envKey} = ${JSON.stringify(env[envKey])}\n`
)).join('')
const componentsPart = Object.keys(components || {}).map((componentId) => {
const componentUrl = path.relative(
absoluteEntryPath,
components[componentId],
)
return [
`import ${componentId} from '${slash(componentUrl)}'`,
`AdminJS.UserComponents.${componentId} = ${componentId}`,
].join('\n')
}).join('\n')
return setupPart + envPart + componentsPart
}
export default generateUserComponentEntry
================================================
FILE: src/backend/bundler/globals.bundler.ts
================================================
import path from 'path'
import * as url from 'url'
import { InputOptions, OutputOptions } from 'rollup'
import { nodeResolve as resolve } from '@rollup/plugin-node-resolve'
import * as commonjs from '@rollup/plugin-commonjs'
import * as replace from '@rollup/plugin-replace'
import * as json from '@rollup/plugin-json'
import * as polyfills from 'rollup-plugin-polyfill-node'
import { minify } from 'rollup-plugin-esbuild-minify'
import { AssetBundler } from './utils/asset-bundler.js'
import { NODE_ENV } from './utils/constants.js'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const input: InputOptions = {
input: path.join(__dirname, '../../frontend/global-entry.js'),
plugins: [
resolve({
extensions: AssetBundler.DEFAULT_EXTENSIONS,
mainFields: ['browser'],
preferBuiltins: false,
browser: true,
}),
(replace as any).default({
'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
'process.env.IS_BROWSER': 'true',
'process.stderr.fd': 'false',
preventAssignment: true,
'process.browser': true,
}),
(json as any).default(),
(commonjs as any).default(),
(polyfills as any).default(),
...(NODE_ENV === 'production' ? [minify()] : []),
],
}
const output: OutputOptions = {
name: 'globals',
inlineDynamicImports: true,
file: path.join(__dirname, `../../frontend/assets/scripts/global-bundle.${NODE_ENV}.js`),
globals: {
react: 'React',
redux: 'Redux',
axios: 'axios',
punycode: 'punycode',
uuid: 'uuid',
'@adminjs/design-system/styled-components': 'styled',
'react-dom': 'ReactDOM',
'prop-types': 'PropTypes',
'react-redux': 'ReactRedux',
'react-router': 'ReactRouter',
'react-router-dom': 'ReactRouterDOM',
},
}
const bundler = new AssetBundler(input, output)
export default bundler
================================================
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/asset-bundler.ts
================================================
/* eslint-disable import/no-extraneous-dependencies */
import { readFile, mkdir, writeFile } from 'fs/promises'
import { rollup, watch, InputOptions, OutputOptions } from 'rollup'
import isUndefined from 'lodash/isUndefined.js'
import ora from 'ora'
import { ADMIN_JS_TMP_DIR, NODE_ENV } from './constants.js'
export class AssetBundler {
static DEFAULT_EXTENSIONS = ['.mjs', '.cjs', '.js', '.jsx', '.json', '.ts', '.tsx', '.scss']
static DEFAULT_GLOBALS = {
react: 'React',
redux: 'Redux',
'react-feather': 'FeatherIcons',
'@adminjs/design-system/styled-components': 'styled',
'prop-types': 'PropTypes',
'react-dom': 'ReactDOM',
'react-redux': 'ReactRedux',
'react-router': 'ReactRouter',
'react-router-dom': 'ReactRouterDOM',
adminjs: 'AdminJS',
'@adminjs/design-system': 'AdminJSDesignSystem',
}
static DEFAULT_EXTERNALS = [
'prop-types',
'react',
'react-dom',
'redux',
'react-redux',
'react-router',
'react-router-dom',
'@adminjs/design-system/styled-components',
'adminjs',
'@adminjs/design-system',
'react-feather',
]
protected inputOptions: InputOptions = {}
protected outputOptions: OutputOptions = {}
constructor(input: InputOptions, output: OutputOptions) {
this.createConfiguration(input, output)
}
public async build() {
const bundle = await rollup(this.inputOptions)
await bundle.write(this.outputOptions)
await bundle.generate(this.outputOptions)
}
public async watch() {
const bundle = await rollup(this.inputOptions)
const spinner = ora(`Bundling files in watchmode: ${JSON.stringify(this.inputOptions)}`)
const watcher = watch({
...this.inputOptions,
output: this.outputOptions,
})
watcher.on('event', (event) => {
if (event.code === 'START') {
spinner.start('Bundling files...')
}
if (event.code === 'ERROR') {
spinner.fail('Bundle fail:')
// eslint-disable-next-line no-console
console.log(event)
}
if (event.code === 'END') {
spinner.succeed(`Finish bundling: ${bundle.watchFiles.length} files`)
}
})
}
public async createEntry({
dir = ADMIN_JS_TMP_DIR,
content,
}: {
write?: boolean
dir?: string
content: string
}) {
try {
await mkdir(dir, { recursive: true })
} catch (error) {
if (error.code !== 'EEXIST') {
throw error
}
}
await writeFile(this.inputOptions.input as string, content)
}
public async getOutput(): Promise<string | null> {
try {
return await readFile(this.outputOptions.file as string, 'utf-8')
} catch (error) {
if (error.code !== 'ENOENT') {
throw error
}
}
return null
}
public createConfiguration(input: InputOptions, output: OutputOptions): void {
this.inputOptions = input
this.outputOptions = output
if (isUndefined(this.inputOptions.input)) {
throw new Error('InputOptions#input must be defined')
}
if (typeof this.inputOptions.input !== 'string') {
throw new Error('InputOptions#input must be a "string"')
}
if (isUndefined(this.outputOptions.file)) {
throw new Error('OutputOptions#file must be defined')
}
if (isUndefined(this.outputOptions.name)) {
throw new Error('OutputOptions#name must be defined')
}
if (isUndefined(this.outputOptions.format)) {
this.outputOptions.format = 'iife'
}
if (isUndefined(this.outputOptions.interop)) {
this.outputOptions.interop = 'auto'
}
if (isUndefined(this.outputOptions.sourcemap)) {
this.outputOptions.sourcemap = NODE_ENV === 'production' ? false : 'inline'
}
}
public setInputOption<T = any>(key: string, value: T) {
this.inputOptions[key] = value
return this
}
public setOutputOption<T = any>(key: string, value: T) {
this.outputOptions[key] = value
return this
}
}
================================================
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/api-controller.spec.js
================================================
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from 'chai'
import ApiController from './api-controller.js'
import { Filter } from '../utils/filter/index.js'
describe('ApiController', function () {
beforeEach(function () {
this.total = 0
this.fieldName = 'title'
this.recordJSON = { title: 'recordTitle' }
this.recordStub = {
toJSON: () => this.recordJSON,
params: {},
recordActions: [],
}
this.resourceName = 'Users'
this.action = {
name: 'actionName',
handler: this.sinon.stub().returns({ record: this.recordStub }),
isAccessible: this.sinon.stub().returns(true),
}
const property = { name: () => this.fieldName, reference: () => false, isId: () => true, type: () => 'string' }
this.resourceStub = {
id: this.sinon.stub().returns('someId'),
decorate: this.sinon.stub().returns({
actions: {
list: this.action,
edit: this.action,
show: this.action,
delete: this.action,
new: this.action,
[this.action.name]: this.action,
},
getListProperties: this.sinon.stub().returns([property]),
titleProperty: () => ({ name: () => this.fieldName }),
properties: { [property.name()]: property },
resourceActions: () => [this.action],
recordActions: () => [this.action],
recordsDecorator: (records) => records,
getFlattenProperties: this.sinon.stub().returns([property]),
id: this.resourceName,
}),
find: this.sinon.stub
gitextract_d7cte0cf/
├── .babelrc.json
├── .cspell.json
├── .dockerignore
├── .eslintrc.cjs
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── feature_request.yml
│ └── workflows/
│ └── push.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .prettierrc
├── .releaserc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── UPGRADE-6.0.md
├── bin/
│ ├── app.js
│ └── globals.js
├── cli.js
├── commitlint.config.cjs
├── cy/
│ ├── commands/
│ │ ├── ab-get-property.js
│ │ ├── ab-keep-logged-in.js
│ │ ├── ab-login-api.js
│ │ └── ab-login.js
│ ├── cypress.doc.md
│ ├── index.d.ts
│ ├── index.js
│ └── readme.md
├── index.d.ts
├── index.js
├── package.json
├── project.inlang/
│ ├── project_id
│ └── settings.json
├── spec/
│ ├── backend/
│ │ ├── helpers/
│ │ │ ├── helper-stub.ts
│ │ │ └── resource-stub.ts
│ │ └── index.js
│ ├── fixtures/
│ │ ├── action.factory.js
│ │ ├── example-component.js
│ │ ├── property.factory.js
│ │ └── record.factory.js
│ ├── index.js
│ ├── lib.js
│ └── setup.js
├── src/
│ ├── adminjs-options.interface.ts
│ ├── adminjs.spec.ts
│ ├── adminjs.ts
│ ├── babel.test.config.json
│ ├── backend/
│ │ ├── actions/
│ │ │ ├── action.interface.ts
│ │ │ ├── bulk-delete/
│ │ │ │ ├── bulk-delete-action.spec.ts
│ │ │ │ └── bulk-delete-action.ts
│ │ │ ├── delete/
│ │ │ │ ├── delete-action.spec.ts
│ │ │ │ └── delete-action.ts
│ │ │ ├── edit/
│ │ │ │ └── edit-action.ts
│ │ │ ├── index.ts
│ │ │ ├── list/
│ │ │ │ └── list-action.ts
│ │ │ ├── new/
│ │ │ │ └── new-action.ts
│ │ │ ├── search/
│ │ │ │ └── search-action.ts
│ │ │ └── show/
│ │ │ └── show-action.ts
│ │ ├── adapters/
│ │ │ ├── database/
│ │ │ │ ├── base-database.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── property/
│ │ │ │ ├── base-property.ts
│ │ │ │ └── index.ts
│ │ │ ├── record/
│ │ │ │ ├── base-record.spec.ts
│ │ │ │ ├── base-record.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── params.type.ts
│ │ │ └── resource/
│ │ │ ├── base-resource.spec.ts
│ │ │ ├── base-resource.ts
│ │ │ ├── index.ts
│ │ │ └── supported-databases.type.ts
│ │ ├── bundler/
│ │ │ ├── app.bundler.ts
│ │ │ ├── components.bundler.ts
│ │ │ ├── generate-user-component-entry.spec.js
│ │ │ ├── generate-user-component-entry.ts
│ │ │ ├── globals.bundler.ts
│ │ │ ├── index.ts
│ │ │ └── utils/
│ │ │ ├── asset-bundler.ts
│ │ │ └── constants.ts
│ │ ├── controllers/
│ │ │ ├── api-controller.spec.js
│ │ │ ├── api-controller.ts
│ │ │ ├── app-controller.ts
│ │ │ └── index.ts
│ │ ├── decorators/
│ │ │ ├── action/
│ │ │ │ ├── action-decorator.spec.ts
│ │ │ │ ├── action-decorator.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── property/
│ │ │ │ ├── index.ts
│ │ │ │ ├── property-decorator.spec.ts
│ │ │ │ ├── property-decorator.ts
│ │ │ │ ├── property-options.interface.ts
│ │ │ │ └── utils/
│ │ │ │ ├── index.ts
│ │ │ │ ├── override-from-options.spec.ts
│ │ │ │ └── override-from-options.ts
│ │ │ └── resource/
│ │ │ ├── index.ts
│ │ │ ├── resource-decorator.spec.ts
│ │ │ ├── resource-decorator.ts
│ │ │ ├── resource-options.interface.ts
│ │ │ └── utils/
│ │ │ ├── decorate-actions.ts
│ │ │ ├── decorate-properties.spec.ts
│ │ │ ├── decorate-properties.ts
│ │ │ ├── find-sub-property.ts
│ │ │ ├── flat-sub-properties.ts
│ │ │ ├── get-navigation.spec.ts
│ │ │ ├── get-navigation.ts
│ │ │ ├── get-property-by-key.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── services/
│ │ │ ├── action-error-handler/
│ │ │ │ ├── action-error-handler.spec.ts
│ │ │ │ ├── action-error-handler.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ └── sort-setter/
│ │ │ ├── index.ts
│ │ │ ├── sort-setter.spec.js
│ │ │ └── sort-setter.ts
│ │ └── utils/
│ │ ├── auth/
│ │ │ ├── base-auth-provider.ts
│ │ │ ├── default-auth-provider.ts
│ │ │ └── index.ts
│ │ ├── build-feature/
│ │ │ ├── build-feature.spec.ts
│ │ │ ├── build-feature.ts
│ │ │ └── index.ts
│ │ ├── component-loader.ts
│ │ ├── errors/
│ │ │ ├── app-error.ts
│ │ │ ├── configuration-error.ts
│ │ │ ├── forbidden-error.ts
│ │ │ ├── index.ts
│ │ │ ├── not-found-error.ts
│ │ │ ├── not-implemented-error.ts
│ │ │ ├── record-error.ts
│ │ │ └── validation-error.ts
│ │ ├── filter/
│ │ │ ├── filter.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── layout-element-parser/
│ │ │ ├── index.ts
│ │ │ ├── layout-element-parser.spec.ts
│ │ │ ├── layout-element-parser.ts
│ │ │ └── layout-element.doc.md
│ │ ├── options-parser/
│ │ │ ├── index.ts
│ │ │ └── options-parser.ts
│ │ ├── populator/
│ │ │ ├── index.ts
│ │ │ ├── populate-property.spec.ts
│ │ │ ├── populate-property.ts
│ │ │ ├── populator.doc.md
│ │ │ ├── populator.spec.ts
│ │ │ └── populator.ts
│ │ ├── request-parser/
│ │ │ ├── index.ts
│ │ │ ├── request-parser.spec.ts
│ │ │ └── request-parser.ts
│ │ ├── resources-factory/
│ │ │ ├── index.ts
│ │ │ ├── resources-factory.spec.js
│ │ │ └── resources-factory.ts
│ │ ├── router/
│ │ │ ├── index.ts
│ │ │ ├── router.doc.md
│ │ │ ├── router.spec.ts
│ │ │ └── router.ts
│ │ ├── uploaded-file.type.ts
│ │ └── view-helpers/
│ │ ├── index.ts
│ │ ├── view-helpers.spec.ts
│ │ └── view-helpers.ts
│ ├── constants.ts
│ ├── core-scripts.interface.ts
│ ├── current-admin.interface.ts
│ ├── frontend/
│ │ ├── assets/
│ │ │ └── styles/
│ │ │ └── icomoon.css
│ │ ├── bundle-entry.jsx
│ │ ├── components/
│ │ │ ├── actions/
│ │ │ │ ├── action.props.ts
│ │ │ │ ├── bulk-delete.tsx
│ │ │ │ ├── edit.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── list.tsx
│ │ │ │ ├── new.tsx
│ │ │ │ ├── show.tsx
│ │ │ │ └── utils/
│ │ │ │ ├── append-force-refresh.spec.ts
│ │ │ │ ├── append-force-refresh.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout-element-renderer.tsx
│ │ │ ├── app/
│ │ │ │ ├── action-button/
│ │ │ │ │ ├── action-button.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── action-header/
│ │ │ │ │ ├── action-header-props.tsx
│ │ │ │ │ ├── action-header.tsx
│ │ │ │ │ ├── actions-to-button-group.spec.ts
│ │ │ │ │ ├── actions-to-button-group.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── styled-back-button.tsx
│ │ │ │ ├── admin-modal.tsx
│ │ │ │ ├── app-loader.tsx
│ │ │ │ ├── auth-background-component.tsx
│ │ │ │ ├── base-action-component.tsx
│ │ │ │ ├── breadcrumbs.tsx
│ │ │ │ ├── default-dashboard.tsx
│ │ │ │ ├── drawer-portal.tsx
│ │ │ │ ├── error-boundary.tsx
│ │ │ │ ├── error-message.tsx
│ │ │ │ ├── filter-drawer.tsx
│ │ │ │ ├── footer.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── language-select/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── language-select.tsx
│ │ │ │ ├── logged-in.tsx
│ │ │ │ ├── notice.tsx
│ │ │ │ ├── records-table/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── no-records.tsx
│ │ │ │ │ ├── property-header.spec.tsx
│ │ │ │ │ ├── property-header.tsx
│ │ │ │ │ ├── record-in-list.tsx
│ │ │ │ │ ├── records-table-header.spec.tsx
│ │ │ │ │ ├── records-table-header.tsx
│ │ │ │ │ ├── records-table.spec.tsx
│ │ │ │ │ ├── records-table.tsx
│ │ │ │ │ ├── selected-records.tsx
│ │ │ │ │ └── utils/
│ │ │ │ │ ├── display.tsx
│ │ │ │ │ ├── get-bulk-actions-from-records.spec.ts
│ │ │ │ │ └── get-bulk-actions-from-records.ts
│ │ │ │ ├── sidebar/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── sidebar-branding.tsx
│ │ │ │ │ ├── sidebar-footer.tsx
│ │ │ │ │ ├── sidebar-pages.tsx
│ │ │ │ │ ├── sidebar-resource-section.tsx
│ │ │ │ │ └── sidebar.tsx
│ │ │ │ ├── sort-link.tsx
│ │ │ │ ├── top-bar.tsx
│ │ │ │ ├── utils/
│ │ │ │ │ ├── discord-logo-svg.tsx
│ │ │ │ │ └── rocket-svg.tsx
│ │ │ │ └── version.tsx
│ │ │ ├── application.tsx
│ │ │ ├── index.ts
│ │ │ ├── login/
│ │ │ │ └── index.tsx
│ │ │ ├── property-type/
│ │ │ │ ├── array/
│ │ │ │ │ ├── add-new-item-translation.tsx
│ │ │ │ │ ├── convert-to-sub-property.tsx
│ │ │ │ │ ├── edit.spec.tsx
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── remove-sub-property.spec.ts
│ │ │ │ │ ├── remove-sub-property.ts
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── base-property-component.doc.md
│ │ │ │ ├── base-property-component.tsx
│ │ │ │ ├── base-property-props.ts
│ │ │ │ ├── boolean/
│ │ │ │ │ ├── boolean-property-value.tsx
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── map-value.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── clean-property-component.tsx
│ │ │ │ ├── currency/
│ │ │ │ │ ├── currency-input-wrapper.tsx
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── format-value.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── datetime/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── map-value.ts
│ │ │ │ │ ├── show.tsx
│ │ │ │ │ └── strip-time-from-iso.ts
│ │ │ │ ├── default-type/
│ │ │ │ │ ├── default-property-value.tsx
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── show.spec.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── docs/
│ │ │ │ │ └── on-property-change.doc.md
│ │ │ │ ├── index.tsx
│ │ │ │ ├── key-value/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── mixed/
│ │ │ │ │ ├── convert-to-sub-property.ts
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── password/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── phone/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── record-property-is-equal.ts
│ │ │ │ ├── reference/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── filter.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── reference-value.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── richtext/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ └── show.tsx
│ │ │ │ ├── textarea/
│ │ │ │ │ ├── edit.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── show.tsx
│ │ │ │ └── utils/
│ │ │ │ ├── index.ts
│ │ │ │ ├── property-description/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── property-description.tsx
│ │ │ │ └── property-label/
│ │ │ │ ├── index.ts
│ │ │ │ └── property-label.tsx
│ │ │ ├── routes/
│ │ │ │ ├── bulk-action.tsx
│ │ │ │ ├── dashboard.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── page.tsx
│ │ │ │ ├── record-action.spec.tsx
│ │ │ │ ├── record-action.tsx
│ │ │ │ ├── resource-action.tsx
│ │ │ │ ├── resource.tsx
│ │ │ │ └── utils/
│ │ │ │ ├── should-action-re-fetch-data.ts
│ │ │ │ └── wrapper.tsx
│ │ │ └── spec/
│ │ │ ├── action-json.factory.ts
│ │ │ ├── factory.ts
│ │ │ ├── initialize-translations.ts
│ │ │ ├── page-json.factory.ts
│ │ │ ├── property-json.factory.ts
│ │ │ ├── record-json.factory.ts
│ │ │ ├── resource-json.factory.ts
│ │ │ └── test-context-provider.tsx
│ │ ├── global-entry.js
│ │ ├── hoc/
│ │ │ ├── allow-override.tsx
│ │ │ ├── index.ts
│ │ │ ├── with-no-ssr.tsx
│ │ │ └── with-notice.ts
│ │ ├── hooks/
│ │ │ ├── index.ts
│ │ │ ├── use-action/
│ │ │ │ ├── index.ts
│ │ │ │ ├── use-action-response-handler.ts
│ │ │ │ ├── use-action.doc.md
│ │ │ │ ├── use-action.ts
│ │ │ │ └── use-action.types.ts
│ │ │ ├── use-current-admin.ts
│ │ │ ├── use-filter-drawer.tsx
│ │ │ ├── use-history-listen.ts
│ │ │ ├── use-local-storage/
│ │ │ │ ├── index.ts
│ │ │ │ ├── use-local-storage-result.type.ts
│ │ │ │ ├── use-local-storage.doc.md
│ │ │ │ └── use-local-storage.ts
│ │ │ ├── use-modal.doc.md
│ │ │ ├── use-modal.ts
│ │ │ ├── use-navigation-resources.ts
│ │ │ ├── use-notice.ts
│ │ │ ├── use-query-params.ts
│ │ │ ├── use-record/
│ │ │ │ ├── filter-record.spec.ts
│ │ │ │ ├── filter-record.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── is-entire-record-given.ts
│ │ │ │ ├── merge-record-response.ts
│ │ │ │ ├── params-to-form-data.spec.ts
│ │ │ │ ├── params-to-form-data.ts
│ │ │ │ ├── update-record.spec.ts
│ │ │ │ ├── update-record.ts
│ │ │ │ ├── use-record.doc.md
│ │ │ │ ├── use-record.tsx
│ │ │ │ └── use-record.type.ts
│ │ │ ├── use-records/
│ │ │ │ ├── index.ts
│ │ │ │ ├── use-records-result.type.ts
│ │ │ │ ├── use-records.doc.md
│ │ │ │ └── use-records.ts
│ │ │ ├── use-resource/
│ │ │ │ ├── index.ts
│ │ │ │ ├── use-resource.doc.md
│ │ │ │ └── use-resource.ts
│ │ │ ├── use-selected-records/
│ │ │ │ ├── index.ts
│ │ │ │ ├── use-selected-records-result.type.ts
│ │ │ │ ├── use-selected-records.doc.md
│ │ │ │ └── use-selected-records.ts
│ │ │ └── use-translation.ts
│ │ ├── index.ts
│ │ ├── interfaces/
│ │ │ ├── action/
│ │ │ │ ├── action-has-component.ts
│ │ │ │ ├── action-href.ts
│ │ │ │ ├── action-json.interface.ts
│ │ │ │ ├── build-action-api-call-trigger.ts
│ │ │ │ ├── build-action-click-handler.ts
│ │ │ │ ├── build-action-test-id.ts
│ │ │ │ ├── call-action-api.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── is-bulk-action.ts
│ │ │ │ ├── is-record-action.ts
│ │ │ │ └── is-resource-action.ts
│ │ │ ├── index.ts
│ │ │ ├── modal.interface.ts
│ │ │ ├── noticeMessage.interface.ts
│ │ │ ├── page-json.interface.ts
│ │ │ ├── property-json/
│ │ │ │ ├── index.ts
│ │ │ │ └── property-json.interface.ts
│ │ │ ├── record-json.interface.ts
│ │ │ └── resource-json.interface.ts
│ │ ├── layout-template.spec.ts
│ │ ├── layout-template.tsx
│ │ ├── login-template.spec.ts
│ │ ├── login-template.tsx
│ │ ├── store/
│ │ │ ├── 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
│ │ │ ├── index.ts
│ │ │ ├── initialize-store.ts
│ │ │ ├── reducers/
│ │ │ │ ├── assetsReducer.ts
│ │ │ │ ├── brandingReducer.ts
│ │ │ │ ├── dashboardReducer.ts
│ │ │ │ ├── drawerReducer.ts
│ │ │ │ ├── filterDrawerReducer.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── localesReducer.ts
│ │ │ │ ├── modalReducer.ts
│ │ │ │ ├── noticesReducer.ts
│ │ │ │ ├── pagesReducer.ts
│ │ │ │ ├── pathsReducer.ts
│ │ │ │ ├── resourcesReducer.ts
│ │ │ │ ├── routerReducer.ts
│ │ │ │ ├── sessionReducer.ts
│ │ │ │ ├── themeReducer.ts
│ │ │ │ └── versionsReducer.ts
│ │ │ ├── store.ts
│ │ │ └── utils/
│ │ │ └── pages-to-store.ts
│ │ └── utils/
│ │ ├── adminjs.i18n.ts
│ │ ├── api-client.ts
│ │ ├── data-css-name.ts
│ │ ├── index.ts
│ │ └── overridable-component.ts
│ ├── index.ts
│ ├── locale/
│ │ ├── config.ts
│ │ ├── de/
│ │ │ └── translation.json
│ │ ├── default-config.ts
│ │ ├── en/
│ │ │ └── translation.json
│ │ ├── es/
│ │ │ └── translation.json
│ │ ├── index.ts
│ │ ├── it/
│ │ │ └── translation.json
│ │ ├── ja/
│ │ │ └── translation.json
│ │ ├── pl/
│ │ │ └── translation.json
│ │ ├── pt-BR/
│ │ │ └── translation.json
│ │ ├── ua/
│ │ │ └── translation.json
│ │ └── zh-CN/
│ │ └── translation.json
│ └── utils/
│ ├── error-type.enum.ts
│ ├── file-resolver.ts
│ ├── flat/
│ │ ├── constants.ts
│ │ ├── filter-out-params.doc.md
│ │ ├── filter-out-params.spec.ts
│ │ ├── filter-out-params.ts
│ │ ├── flat-module.ts
│ │ ├── flat.doc.md
│ │ ├── flat.types.ts
│ │ ├── get.doc.md
│ │ ├── get.spec.ts
│ │ ├── get.ts
│ │ ├── index.ts
│ │ ├── merge.spec.ts
│ │ ├── merge.ts
│ │ ├── path-parts.type.ts
│ │ ├── path-to-parts.doc.md
│ │ ├── path-to-parts.ts
│ │ ├── property-key-regex.ts
│ │ ├── remove-path.doc.md
│ │ ├── remove-path.spec.ts
│ │ ├── remove-path.ts
│ │ ├── select-params.doc.md
│ │ ├── select-params.spec.ts
│ │ ├── select-params.ts
│ │ ├── set.doc.md
│ │ ├── set.spec.ts
│ │ └── set.ts
│ ├── index.ts
│ ├── param-converter/
│ │ ├── constants.ts
│ │ ├── convert-nested-param.spec.ts
│ │ ├── convert-nested-param.ts
│ │ ├── convert-param.spec.ts
│ │ ├── convert-param.ts
│ │ ├── index.ts
│ │ ├── param-converter-module.ts
│ │ ├── prepare-params.ts
│ │ └── validate-param.ts
│ ├── theme-bundler.ts
│ └── translate-functions.factory.ts
├── tsconfig.json
└── vendor-types/
├── chai/
│ └── index.d.ts
├── global.ts
└── node/
└── node.d.ts
SYMBOL INDEX (564 symbols across 184 files)
FILE: cy/index.d.ts
type AbLoginParams (line 4) | type AbLoginParams = {
type AbKeepLoggedInParams (line 10) | type AbKeepLoggedInParams = {
type Chainable (line 14) | interface Chainable<Subject> {
FILE: spec/fixtures/example-component.js
function ExampleComponent (line 3) | function ExampleComponent() {
FILE: src/adminjs-options.interface.ts
type AdminJSOptions (line 55) | interface AdminJSOptions {
type ThemeConfig (line 251) | type ThemeConfig = {
type AdminJSSettings (line 259) | type AdminJSSettings = {
type Assets (line 274) | type Assets = {
type AssetsFunction (line 298) | type AssetsFunction = (admin?: CurrentAdmin) => Assets | Promise<Assets>;
type VersionSettings (line 305) | type VersionSettings = {
type VersionProps (line 317) | type VersionProps = {
type BrandingOptions (line 340) | type BrandingOptions = {
type BrandingOptionsFunction (line 375) | type BrandingOptionsFunction = (
type AdminPage (line 385) | type AdminPage = {
type AdminPages (line 407) | type AdminPages = Record<string, AdminPage>;
type ResourceWithOptions (line 414) | type ResourceWithOptions = {
type FeatureType (line 428) | type FeatureType = (
type PageHandler (line 445) | type PageHandler = (request: any, response: any, context: PageContext) =...
type BundlerOptions (line 459) | type BundlerOptions = {
type AdminJSOptionsWithDefault (line 466) | interface AdminJSOptionsWithDefault extends AdminJSOptions {
FILE: src/adminjs.spec.ts
class Database (line 25) | class Database extends BaseDatabase {}
class Resource (line 26) | class Resource extends BaseResource {}
FILE: src/adminjs.ts
constant VERSION (line 33) | const VERSION = pkg.version
type ActionsMap (line 47) | type ActionsMap = {
type Adapter (line 56) | type Adapter = { Database: typeof BaseDatabase; Resource: typeof BaseRes...
class AdminJS (line 69) | class AdminJS {
method constructor (line 98) | constructor(options: AdminJSOptions = {}) {
method registerAdapter (line 136) | static registerAdapter({
method initialize (line 164) | async initialize(): Promise<void> {
method watch (line 181) | async watch(): Promise<string | undefined> {
method renderLogin (line 205) | async renderLogin(props: LoginTemplateAttributes): Promise<string> {
method findResource (line 220) | findResource(resourceId): BaseResource {
method resolveBabelConfigPath (line 238) | resolveBabelConfigPath(): void {
method addThemeAssets (line 281) | addThemeAssets() {
type AdminJS (line 303) | interface AdminJS extends TranslateFunctions {}
method constructor (line 98) | constructor(options: AdminJSOptions = {}) {
method registerAdapter (line 136) | static registerAdapter({
method initialize (line 164) | async initialize(): Promise<void> {
method watch (line 181) | async watch(): Promise<string | undefined> {
method renderLogin (line 205) | async renderLogin(props: LoginTemplateAttributes): Promise<string> {
method findResource (line 220) | findResource(resourceId): BaseResource {
method resolveBabelConfigPath (line 238) | resolveBabelConfigPath(): void {
method addThemeAssets (line 281) | addThemeAssets() {
FILE: src/backend/actions/action.interface.ts
type ActionQueryParameters (line 13) | type ActionQueryParameters = {
type ActionType (line 21) | type ActionType = 'resource' | 'record' | 'bulk'
type ActionContext (line 30) | type ActionContext = {
type PageContext (line 71) | type PageContext = {
type ActionRequest (line 91) | type ActionRequest = {
type ActionResponse (line 138) | type ActionResponse = {
type IsFunction (line 159) | type IsFunction = (context: ActionContext) => boolean
type RecordActionResponse (line 167) | type RecordActionResponse = ActionResponse & {
type BulkActionResponse (line 180) | type BulkActionResponse = ActionResponse & {
type ActionHandler (line 196) | type ActionHandler<T> = (
type Before (line 210) | type Before = (
type After (line 228) | type After<T> = (
type BuildInActions (line 243) | type BuildInActions =
type Action (line 315) | interface Action <T extends ActionResponse> {
FILE: src/backend/actions/index.ts
constant ACTIONS (line 19) | const ACTIONS: {[key in BuildInActions]: any} = {
FILE: src/backend/actions/list/list-action.ts
constant PER_PAGE_LIMIT (line 8) | const PER_PAGE_LIMIT = 500
type ListActionResponse (line 91) | type ListActionResponse = ActionResponse & {
FILE: src/backend/actions/search/search-action.ts
type SearchActionResponse (line 74) | type SearchActionResponse = ActionResponse & {
FILE: src/backend/adapters/database/base-database.ts
class BaseDatabase (line 19) | class BaseDatabase {
method constructor (line 20) | constructor(database: any) {}
method isAdapterFor (line 28) | static isAdapterFor(database: any): boolean {
method resources (line 37) | resources(): Array<BaseResource> {
FILE: src/backend/adapters/property/base-property.ts
constant TITLE_COLUMN_NAMES (line 22) | const TITLE_COLUMN_NAMES = ['title', 'name', 'subject', 'email']
type PropertyType (line 24) | type PropertyType =
type BasePropertyAttrs (line 30) | type BasePropertyAttrs = {
class BaseProperty (line 42) | class BaseProperty {
method constructor (line 63) | constructor({
method name (line 84) | name(): string {
method path (line 88) | path(): string {
method position (line 92) | position(): number {
method type (line 100) | type(): PropertyType {
method isTitle (line 109) | isTitle(): boolean {
method isVisible (line 118) | isVisible(): boolean {
method isEditable (line 127) | isEditable(): boolean {
method isId (line 136) | isId(): boolean {
method reference (line 147) | reference(): string | null {
method availableValues (line 158) | availableValues(): Array<string> | null {
method isArray (line 167) | isArray(): boolean {
method isDraggable (line 177) | isDraggable(): boolean {
method subProperties (line 186) | subProperties(): Array<BaseProperty> {
method isSortable (line 195) | isSortable(): boolean {
method isRequired (line 202) | isRequired(): boolean {
FILE: src/backend/adapters/record/base-record.ts
class BaseRecord (line 15) | class BaseRecord {
method constructor (line 47) | constructor(params: ParamsType, resource: BaseResource) {
method param (line 63) | param(path: string): any {
method get (line 85) | get(propertyPath?: string, options?: GetOptions): any {
method set (line 98) | set(propertyPath: string, value: any): any {
method namespaceParams (line 111) | namespaceParams(prefix: string): Record<string, any> | void {
method selectParams (line 124) | selectParams(prefix: string, options?: GetOptions): Record<string, any...
method update (line 138) | async update(params, context?: ActionContext): Promise<BaseRecord> {
method save (line 167) | async save(context?: ActionContext): Promise<BaseRecord> {
method create (line 201) | async create(context?: ActionContext): Promise<BaseRecord> {
method id (line 222) | id(): string {
method title (line 238) | title(): string {
method isValid (line 247) | isValid(): boolean {
method error (line 256) | error(path: string): RecordError | null {
method populate (line 267) | populate(propertyPath: string, record?: BaseRecord | null): void {
method toJSON (line 282) | toJSON(currentAdmin?: CurrentAdmin): RecordJSON {
method storeParams (line 313) | storeParams(payloadData?: object): void {
FILE: src/backend/adapters/record/params.type.ts
type ParamsTypeValue (line 5) | type ParamsTypeValue = string
type ParamsType (line 18) | type ParamsType = Record<string, any>
FILE: src/backend/adapters/resource/base-resource.ts
class BaseResource (line 38) | class BaseResource {
method isAdapterFor (line 52) | static isAdapterFor(rawResource): boolean {
method constructor (line 61) | constructor(resource?: any) {
method databaseName (line 73) | databaseName(): string {
method databaseType (line 82) | databaseType(): SupportedDatabasesType | string {
method id (line 92) | id(): string {
method properties (line 101) | properties(): Array<BaseProperty> {
method property (line 113) | property(path: string): BaseProperty | null {
method count (line 124) | async count(filter: Filter, context?: ActionContext): Promise<number> {
method find (line 148) | async find(filter: Filter, options: {
method findOne (line 167) | async findOne(id: string, context?: ActionContext): Promise<BaseRecord...
method findMany (line 179) | async findMany(ids: Array<string | number>, context?: ActionContext):
method build (line 195) | build(params: Record<string, any>): BaseRecord {
method create (line 209) | async create(params: Record<string, any>, context?: ActionContext): Pr...
method update (line 224) | async update(id: string, params: Record<string, any>, context?: Action...
method delete (line 237) | async delete(id: string, context?: ActionContext): Promise<void> {
method assignDecorator (line 250) | assignDecorator(admin: AdminJS, options: ResourceOptions = {}): void {
method decorate (line 258) | decorate(): ResourceDecorator {
FILE: src/backend/adapters/resource/supported-databases.type.ts
type SupportedDatabasesType (line 1) | type SupportedDatabasesType = 'MySQL'
FILE: src/backend/bundler/utils/asset-bundler.ts
class AssetBundler (line 9) | class AssetBundler {
method constructor (line 44) | constructor(input: InputOptions, output: OutputOptions) {
method build (line 48) | public async build() {
method watch (line 55) | public async watch() {
method createEntry (line 81) | public async createEntry({
method getOutput (line 99) | public async getOutput(): Promise<string | null> {
method createConfiguration (line 111) | public createConfiguration(input: InputOptions, output: OutputOptions)...
method setInputOption (line 144) | public setInputOption<T = any>(key: string, value: T) {
method setOutputOption (line 150) | public setOutputOption<T = any>(key: string, value: T) {
FILE: src/backend/bundler/utils/constants.ts
constant NODE_ENV (line 3) | const NODE_ENV = process.env.NODE_ENV === 'production' ? 'production' : ...
constant DEFAULT_TMP_DIR (line 5) | const DEFAULT_TMP_DIR = '.adminjs'
constant ADMIN_JS_TMP_DIR (line 6) | const ADMIN_JS_TMP_DIR = typeof process === 'object'
constant COMPONENTS_ENTRY_PATH (line 10) | const COMPONENTS_ENTRY_PATH = path.join(ADMIN_JS_TMP_DIR, 'entry.js')
constant COMPONENTS_OUTPUT_PATH (line 11) | const COMPONENTS_OUTPUT_PATH = path.join(ADMIN_JS_TMP_DIR, 'bundle.js')
FILE: src/backend/controllers/api-controller.ts
class ApiController (line 49) | class ApiController {
method constructor (line 59) | constructor({ admin }, currentAdmin) {
method getActionContext (line 71) | async getActionContext(request: ActionRequest): Promise<ActionContext> {
method search (line 95) | async search(request: ActionRequest, response): Promise<SearchActionRe...
method resourceAction (line 116) | async resourceAction(originalRequest: ActionRequest, response: any): P...
method recordAction (line 135) | async recordAction(originalRequest: ActionRequest, response: any): Pro...
method bulkAction (line 204) | async bulkAction(originalRequest: ActionRequest, response: any): Promi...
method dashboard (line 246) | async dashboard(request: any, response: any): Promise<any> {
method page (line 275) | async page(request: any, response: any): Promise<any> {
FILE: src/backend/controllers/app-controller.ts
class AppController (line 11) | class AppController {
method constructor (line 18) | constructor({ admin }, currentAdmin) {
method index (line 24) | async index(): Promise<string> {
method resourceAction (line 28) | async resourceAction({ params }: ActionRequest): Promise<string> {
method bulkAction (line 34) | async bulkAction({ params, query }: ActionRequest): Promise<string> {
method resource (line 45) | async resource({ params }: ActionRequest): Promise<string> {
method recordAction (line 51) | async recordAction({ params }: ActionRequest): Promise<string> {
method page (line 60) | async page({ params }: ActionRequest): Promise<string> {
method bundleComponents (line 69) | async bundleComponents(): Promise<string | null> {
FILE: src/backend/decorators/action/action-decorator.ts
constant DEFAULT_VARIANT (line 28) | const DEFAULT_VARIANT: VariantType = 'default'
class ActionDecorator (line 35) | class ActionDecorator {
method constructor (line 52) | constructor({ action, admin, resource }: {
method handler (line 84) | async handler(
method invokeBeforeHook (line 107) | async invokeBeforeHook(request: ActionRequest, context: ActionContext)...
method invokeHandler (line 136) | async invokeHandler(
method invokeAfterHook (line 167) | async invokeAfterHook(
method isRecordType (line 196) | isRecordType(): boolean {
method isResourceType (line 205) | isResourceType(): boolean {
method isBulkType (line 214) | isBulkType(): boolean {
method is (line 218) | is(what: 'isAccessible' | 'isVisible', currentAdmin?: CurrentAdmin, re...
method isVisible (line 247) | isVisible(currentAdmin?: CurrentAdmin, record?: BaseRecord): boolean {
method isAccessible (line 258) | isAccessible(currentAdmin?: CurrentAdmin, record?: BaseRecord): boolean {
method canInvokeAction (line 270) | canInvokeAction(context: ActionContext): boolean {
method containerWidth (line 288) | containerWidth(): ActionJSON['containerWidth'] {
method layout (line 297) | layout(currentAdmin?: CurrentAdmin): Array<ParsedLayoutElement> | null {
method variant (line 310) | variant(): VariantType {
method parent (line 314) | parent(): string | null {
method custom (line 318) | custom(): Record<string, any> {
method hasHandler (line 322) | hasHandler(): boolean {
method showResourceActions (line 326) | showResourceActions(): boolean {
method toJSON (line 339) | toJSON(currentAdmin?: CurrentAdmin): ActionJSON {
FILE: src/backend/decorators/property/property-decorator.ts
class PropertyDecorator (line 14) | class PropertyDecorator {
method constructor (line 55) | constructor({ property, admin, options = {}, resource, path, isVirtual...
method isSortable (line 82) | isSortable(): boolean {
method reference (line 91) | reference(): BaseResource | null {
method referenceName (line 100) | referenceName(): string | null {
method name (line 109) | name(): string {
method resource (line 116) | resource(): ResourceDecorator {
method label (line 125) | label(): string {
method type (line 134) | type(): PropertyType {
method availableValues (line 147) | availableValues(): null | Array<{ value: string | number; label?: stri...
method isArray (line 158) | isArray(): boolean {
method isDraggable (line 165) | isDraggable(): boolean {
method isVisible (line 177) | isVisible(where: PropertyPlace): boolean {
method position (line 195) | position(): number {
method isId (line 209) | isId(): boolean {
method isRequired (line 218) | isRequired(): boolean {
method isTitle (line 229) | isTitle(): boolean {
method isDisabled (line 238) | isDisabled(): boolean {
method toJSON (line 249) | toJSON(where?: PropertyPlace): BasePropertyJSON {
method subProperties (line 284) | subProperties(): Array<PropertyDecorator> {
method addSubProperty (line 299) | addSubProperty(subProperty: PropertyDecorator): void {
method getOptionsForSubProperty (line 311) | private getOptionsForSubProperty(propertyPath: string): PropertyOptions {
FILE: src/backend/decorators/property/property-options.interface.ts
type PropertyOptions (line 6) | interface PropertyOptions {
FILE: src/backend/decorators/property/utils/override-from-options.ts
type OverridableFromOptionsType (line 4) | type OverridableFromOptionsType = keyof Pick<BaseProperty,
function overrideFromOptions (line 12) | function overrideFromOptions<T extends OverridableFromOptionsType>(
FILE: src/backend/decorators/resource/resource-decorator.ts
constant DEFAULT_MAX_COLUMNS_IN_LIST (line 24) | const DEFAULT_MAX_COLUMNS_IN_LIST = 8
class ResourceDecorator (line 31) | class ResourceDecorator {
method constructor (line 67) | constructor({ resource, admin, options = {} }: {
method getResourceName (line 102) | getResourceName(): string {
method id (line 110) | id(): string {
method getNavigation (line 119) | getNavigation(): ResourceJSON['navigation'] {
method getPropertyByKey (line 130) | getPropertyByKey(propertyPath: string): PropertyDecorator | null {
method getProperties (line 144) | getProperties({ where, max = 0 }: {
method getFlattenProperties (line 182) | getFlattenProperties(): Record<string, PropertyDecorator> {
method getListProperties (line 191) | getListProperties(): Array<PropertyDecorator> {
method resourceActions (line 202) | resourceActions(currentAdmin?: CurrentAdmin): Array<ActionDecorator> {
method bulkActions (line 218) | bulkActions(record: BaseRecord, currentAdmin?: CurrentAdmin): Array<Ac...
method recordActions (line 234) | recordActions(record: BaseRecord, currentAdmin?: CurrentAdmin): Array<...
method titleProperty (line 248) | titleProperty(): PropertyDecorator {
method titleOf (line 271) | titleOf(record: BaseRecord): string {
method getHref (line 275) | getHref(currentAdmin?: CurrentAdmin): string | null {
method toJSON (line 299) | toJSON(currentAdmin?: CurrentAdmin): ResourceJSON {
FILE: src/backend/decorators/resource/resource-options.interface.ts
type HrefContext (line 16) | type HrefContext = {
type HrefFunction (line 37) | type HrefFunction = (context: HrefContext) => string
type ResourceOptions (line 48) | interface ResourceOptions {
FILE: src/backend/decorators/resource/utils/decorate-actions.ts
type DecoratedActions (line 9) | type DecoratedActions = {[key: string]: ActionDecorator}
function mergeCustomizer (line 11) | function mergeCustomizer<T>(destValue: T | Array<T>, sourceValue: T | Ar...
function decorateActions (line 25) | function decorateActions(
FILE: src/backend/decorators/resource/utils/decorate-properties.ts
type DecoratedProperties (line 8) | type DecoratedProperties = {[key: string]: PropertyDecorator}
function decorateProperties (line 110) | function decorateProperties(
FILE: src/backend/decorators/resource/utils/get-navigation.ts
type DatabaseData (line 5) | type DatabaseData = {
constant DEFAULT_ICON (line 10) | const DEFAULT_ICON = 'Archive'
type IconMapType (line 12) | type IconMapType = {[key in SupportedDatabasesType]: string}
FILE: src/backend/services/sort-setter/sort-setter.ts
constant DEFAULT_DIRECTION (line 4) | const DEFAULT_DIRECTION = 'asc'
type Sort (line 6) | type Sort = {
FILE: src/backend/utils/auth/base-auth-provider.ts
type AuthenticatePayload (line 8) | interface AuthenticatePayload {
type AuthProviderConfig (line 12) | interface AuthProviderConfig<T extends AuthenticatePayload> {
type LoginHandlerOptions (line 17) | interface LoginHandlerOptions {
type RefreshTokenHandlerOptions (line 25) | interface RefreshTokenHandlerOptions extends LoginHandlerOptions {}
class BaseAuthProvider (line 32) | class BaseAuthProvider<TContext = any> {
method getUiProps (line 39) | public getUiProps(): Record<string, any> {
method handleLogin (line 49) | public async handleLogin(opts: LoginHandlerOptions, context?: TContext...
method handleLogout (line 61) | public async handleLogout(context?: TContext): Promise<any> {
method handleRefreshToken (line 80) | public async handleRefreshToken(opts: RefreshTokenHandlerOptions, cont...
FILE: src/backend/utils/auth/default-auth-provider.ts
type DefaultAuthenticatePayload (line 3) | interface DefaultAuthenticatePayload extends AuthenticatePayload {
type DefaultAuthProviderConfig (line 9) | interface DefaultAuthProviderConfig extends AuthProviderConfig<DefaultAu...
class DefaultAuthProvider (line 11) | class DefaultAuthProvider extends BaseAuthProvider {
method constructor (line 14) | constructor({ authenticate }: DefaultAuthProviderConfig) {
method handleLogin (line 19) | override async handleLogin(opts: LoginHandlerOptions, context) {
FILE: src/backend/utils/build-feature/build-feature.ts
function mergeActionHooks (line 10) | function mergeActionHooks<T>(
type BasicOption (line 38) | type BasicOption = typeof basicOptions[number]
type ListOption (line 39) | type ListOption = typeof listOptions[number]
type MissingKeys (line 41) | type MissingKeys = Required<Omit<ResourceOptions, BasicOption | ListOpti...
FILE: src/backend/utils/component-loader.ts
type ComponentDetails (line 7) | interface ComponentDetails {
class ComponentLoader (line 12) | class ComponentLoader {
method add (line 15) | public add(name: string, filePath: string, caller = 'add') {
method override (line 29) | public override(name: string, filePath: string, caller = 'override') {
method __unsafe_addWithoutChecks (line 43) | public __unsafe_addWithoutChecks(name: string, filePath: string, calle...
method clear (line 52) | public clear() {
method getComponents (line 56) | public getComponents() {
method __unsafe_merge (line 68) | public __unsafe_merge(componentLoader: ComponentLoader): void {
method resolveFilePath (line 75) | public static resolveFilePath(filePath: string, caller: string): string {
FILE: src/backend/utils/errors/app-error.ts
class AppError (line 10) | class AppError extends Error {
method constructor (line 40) | constructor(message: string, data?: Record<string, unknown>, notice?: ...
FILE: src/backend/utils/errors/configuration-error.ts
class ConfigurationError (line 13) | class ConfigurationError extends Error {
method constructor (line 19) | constructor(message, fnName) {
FILE: src/backend/utils/errors/forbidden-error.ts
class ForbiddenError (line 10) | class ForbiddenError extends Error {
method constructor (line 29) | constructor(message?: string) {
FILE: src/backend/utils/errors/not-found-error.ts
class NotFoundError (line 14) | class NotFoundError extends Error {
method constructor (line 35) | constructor(message, fnName) {
FILE: src/backend/utils/errors/not-implemented-error.ts
class NotImplementedError (line 23) | class NotImplementedError extends Error {
method constructor (line 28) | constructor(fnName: string) {
FILE: src/backend/utils/errors/record-error.ts
type RecordError (line 8) | type RecordError = {
FILE: src/backend/utils/errors/validation-error.ts
type PropertyErrors (line 9) | type PropertyErrors = {
class ValidationError (line 17) | class ValidationError extends Error {
method constructor (line 38) | constructor(propertyErrors: PropertyErrors, baseError?: RecordError) {
FILE: src/backend/utils/filter/filter.ts
constant PARAM_SEPARATOR (line 7) | const PARAM_SEPARATOR = '~~'
type FilterElement (line 9) | type FilterElement = {
type ReduceCallback (line 19) | interface ReduceCallback<T> {
class Filter (line 27) | class Filter {
method normalizeKeys (line 52) | static normalizeKeys(filters): Map<string, any> {
method constructor (line 60) | constructor(filters = {}, resource) {
method get (line 80) | get(key: string): FilterElement | null {
method populate (line 87) | async populate(context: ActionContext): Promise<Filter> {
method reduce (line 100) | reduce<T>(callback: ReduceCallback<T>, initial: T): T {
method isVisible (line 104) | isVisible(): boolean {
FILE: src/backend/utils/layout-element-parser/layout-element-parser.ts
type LayoutElement (line 16) | type LayoutElement =
type LayoutElementFunction (line 43) | type LayoutElementFunction = (currentAdmin?: CurrentAdmin) => Array<Layo...
type ParsedLayoutElement (line 51) | type ParsedLayoutElement = {
FILE: src/backend/utils/populator/populate-property.ts
function populateProperty (line 19) | async function populateProperty(
FILE: src/backend/utils/populator/populator.ts
function populator (line 11) | async function populator(
FILE: src/backend/utils/resources-factory/resources-factory.spec.js
class Database (line 23) | class Database extends BaseDatabase {
method isAdapterFor (line 24) | static isAdapterFor(database) { return database === 'supported' }
method resources (line 26) | resources() { return new Array(5) }
class Resource (line 28) | class Resource extends BaseResource {}
method isAdapterFor (line 64) | static isAdapterFor(resource) { return resource === 'supported' }
class MyResource (line 55) | class MyResource extends BaseResource {}
class Database (line 62) | class Database extends BaseDatabase {}
method isAdapterFor (line 24) | static isAdapterFor(database) { return database === 'supported' }
method resources (line 26) | resources() { return new Array(5) }
class Resource (line 63) | class Resource extends BaseResource {
method isAdapterFor (line 64) | static isAdapterFor(resource) { return resource === 'supported' }
FILE: src/backend/utils/resources-factory/resources-factory.ts
class NoDatabaseAdapterError (line 6) | class NoDatabaseAdapterError extends Error {
method constructor (line 9) | constructor(database: string) {
class NoResourceAdapterError (line 17) | class NoResourceAdapterError extends Error {
method constructor (line 20) | constructor(resource: BaseResource) {
class ResourcesFactory (line 28) | class ResourcesFactory {
method constructor (line 33) | constructor(admin, adapters: Array<Adapter> = []) {
method buildResources (line 38) | buildResources({ databases, resources }): Array<BaseResource> {
method _convertDatabases (line 55) | _convertDatabases(databases: Array<any>): Array<BaseResource> {
method _convertResources (line 83) | _convertResources(resources: Array<any | ResourceWithOptions>): Array<...
method _decorateResources (line 112) | _decorateResources(resources: Array<ResourceWithOptions>): Array<BaseR...
FILE: src/backend/utils/router/router.ts
constant ASSETS_ROOT (line 10) | const ASSETS_ROOT = `${__dirname}/../lib/../../../frontend/assets/`
type RouterType (line 31) | type RouterType = {
FILE: src/backend/utils/uploaded-file.type.ts
type UploadedFile (line 7) | type UploadedFile = {
FILE: src/backend/utils/view-helpers/view-helpers.ts
type Paths (line 4) | type Paths = PathsInState
type ActionParams (line 25) | type ActionParams = {
type RecordActionParams (line 46) | type RecordActionParams = ActionParams & {
type BulkActionParams (line 59) | type BulkActionParams = ActionParams & {
type ResourceActionParams (line 72) | type ResourceActionParams = ActionParams
class ViewHelpers (line 79) | class ViewHelpers {
method constructor (line 82) | constructor({ options }: { options?: AdminJSOptions } = {}) {
method getPaths (line 93) | static getPaths(options?: AdminJSOptions): Paths {
method urlBuilder (line 105) | urlBuilder(paths: Array<string> = [], search = ''): string {
method loginUrl (line 122) | loginUrl(): string {
method logoutUrl (line 130) | logoutUrl(): string {
method dashboardUrl (line 138) | dashboardUrl(): string {
method pageUrl (line 148) | pageUrl(pageName: string): string {
method editUrl (line 159) | editUrl(resourceId: string, recordId: string, search?: string): string {
method showUrl (line 170) | showUrl(resourceId: string, recordId: string, search?: string): string {
method deleteUrl (line 181) | deleteUrl(resourceId: string, recordId: string, search?: string): stri...
method newUrl (line 191) | newUrl(resourceId: string, search?: string): string {
method listUrl (line 201) | listUrl(resourceId: string, search?: string): string {
method bulkDeleteUrl (line 212) | bulkDeleteUrl(resourceId: string, recordIds: Array<string>, search?: s...
method resourceActionUrl (line 226) | resourceActionUrl({ resourceId, actionName, search }: ResourceActionPa...
method resourceUrl (line 230) | resourceUrl({ resourceId, search }: Omit<ResourceActionParams, 'action...
method recordActionUrl (line 244) | recordActionUrl({ resourceId, recordId, actionName, search }: RecordAc...
method bulkActionUrl (line 258) | bulkActionUrl({ resourceId, recordIds, actionName, search }: BulkActio...
method assetPath (line 276) | assetPath(asset: string, assetsConfig?: Assets): string {
FILE: src/constants.ts
constant DOCS (line 2) | const DOCS = 'https://docs.adminjs.co'
constant DEFAULT_PATHS (line 3) | const DEFAULT_PATHS = {
FILE: src/core-scripts.interface.ts
type CoreScripts (line 24) | interface CoreScripts {
FILE: src/current-admin.interface.ts
type CurrentAdmin (line 13) | interface CurrentAdmin {
FILE: src/frontend/components/actions/action.props.ts
type ActionProps (line 10) | type ActionProps = {
FILE: src/frontend/components/actions/utils/append-force-refresh.ts
constant REFRESH_KEY (line 1) | const REFRESH_KEY = 'refresh'
constant IGNORE_PARAMS_KEY (line 2) | const IGNORE_PARAMS_KEY = 'ignore_params'
FILE: src/frontend/components/actions/utils/layout-element-renderer.tsx
type Props (line 10) | type Props = ActionProps & {
FILE: src/frontend/components/app/action-button/action-button.tsx
type ActionButtonProps (line 14) | type ActionButtonProps = {
FILE: src/frontend/components/app/action-header/action-header-props.tsx
type ActionHeaderProps (line 8) | type ActionHeaderProps = {
FILE: src/frontend/components/app/action-header/actions-to-button-group.ts
type actionsToButtonGroupOptions (line 7) | type actionsToButtonGroupOptions = {
FILE: src/frontend/components/app/action-header/styled-back-button.tsx
type StyledBackButtonProps (line 16) | type StyledBackButtonProps = {
FILE: src/frontend/components/app/breadcrumbs.tsx
type BreadcrumbProps (line 63) | type BreadcrumbProps = {
FILE: src/frontend/components/app/default-dashboard.tsx
type BoxType (line 37) | type BoxType = {
FILE: src/frontend/components/app/drawer-portal.tsx
type DrawerPortalProps (line 16) | type DrawerPortalProps = {
type DrawerWrapperProps (line 28) | type DrawerWrapperProps = {
constant DRAWER_PORTAL_ID (line 33) | const DRAWER_PORTAL_ID = 'drawerPortal'
constant DRAWER_PORTAL_WRAPPER_ID (line 34) | const DRAWER_PORTAL_WRAPPER_ID = 'drawerPortalWrapper'
FILE: src/frontend/components/app/error-boundary.tsx
type State (line 6) | type State = {
class ErrorBoundary (line 20) | class ErrorBoundary extends React.Component<any, State> {
method constructor (line 21) | constructor(props) {
method componentDidCatch (line 28) | componentDidCatch(error): void {
method render (line 32) | render(): ReactNode {
FILE: src/frontend/components/app/error-message.tsx
type ErrorMessageBoxProps (line 10) | type ErrorMessageBoxProps = {
FILE: src/frontend/components/app/filter-drawer.tsx
type FilterProps (line 15) | type FilterProps = {
type MatchProps (line 19) | type MatchProps = {
FILE: src/frontend/components/app/logged-in.tsx
type LoggedInProps (line 8) | type LoggedInProps = {
FILE: src/frontend/components/app/notice.tsx
constant TIME_TO_DISAPPEAR (line 11) | const TIME_TO_DISAPPEAR = 3
type NotifyProgress (line 13) | type NotifyProgress = (options: SetNoticeProgress) => void
type NoticeElementProps (line 15) | type NoticeElementProps = {
type NoticeElementState (line 21) | type NoticeElementState = {
type NoticeBoxPropsFromState (line 70) | type NoticeBoxPropsFromState = {
type NoticeBoxDispatchFromState (line 74) | type NoticeBoxDispatchFromState = {
FILE: src/frontend/components/app/records-table/no-records.tsx
type NoRecordsProps (line 9) | type NoRecordsProps = {
FILE: src/frontend/components/app/records-table/property-header.tsx
type PropertyHeaderProps (line 9) | type PropertyHeaderProps = {
FILE: src/frontend/components/app/records-table/record-in-list.tsx
type RecordInListProps (line 17) | type RecordInListProps = {
FILE: src/frontend/components/app/records-table/records-table-header.tsx
type RecordsTableHeaderProps (line 14) | type RecordsTableHeaderProps = {
FILE: src/frontend/components/app/records-table/records-table.spec.tsx
type StubsType (line 17) | type StubsType = {
FILE: src/frontend/components/app/records-table/records-table.tsx
type RecordsTableProps (line 17) | type RecordsTableProps = {
FILE: src/frontend/components/app/records-table/selected-records.tsx
type SelectedRecordsProps (line 12) | type SelectedRecordsProps = {
FILE: src/frontend/components/app/sidebar/sidebar-branding.tsx
type Props (line 10) | type Props = {
FILE: src/frontend/components/app/sidebar/sidebar-pages.tsx
type Props (line 10) | type Props = {
FILE: src/frontend/components/app/sidebar/sidebar-resource-section.tsx
type SidebarResourceSectionProps (line 13) | type SidebarResourceSectionProps = {
FILE: src/frontend/components/app/sidebar/sidebar.tsx
constant SIDEBAR_Z_INDEX (line 13) | const SIDEBAR_Z_INDEX = 50
type Props (line 15) | type Props = {
FILE: src/frontend/components/app/sort-link.tsx
type SortLinkProps (line 8) | type SortLinkProps = {
FILE: src/frontend/components/app/top-bar.tsx
type Props (line 26) | type Props = {
FILE: src/frontend/components/app/version.tsx
type Props (line 9) | type Props = {
FILE: src/frontend/components/login/index.tsx
type LoginProps (line 49) | type LoginProps = {
FILE: src/frontend/components/property-type/array/add-new-item-translation.tsx
type AddNewItemButtonProps (line 7) | type AddNewItemButtonProps = {
FILE: src/frontend/components/property-type/array/edit.tsx
type EditProps (line 15) | type EditProps = Required<EditPropertyPropsInArray>
type ItemRendererProps (line 17) | type ItemRendererProps = {
FILE: src/frontend/components/property-type/array/show.tsx
type Props (line 10) | type Props = ShowPropertyProps & {
FILE: src/frontend/components/property-type/base-property-props.ts
type SelectRecord (line 6) | type SelectRecord = {
type BasePropertyProps (line 51) | type BasePropertyProps = {
type BasePropertyComponentProps (line 79) | type BasePropertyComponentProps = Omit<BasePropertyProps, 'property'> & {
type BasePropertyPropsExtended (line 83) | type BasePropertyPropsExtended = BasePropertyProps & {
type FilterPropertyProps (line 95) | type FilterPropertyProps = BasePropertyProps & {
type EditPropertyProps (line 114) | type EditPropertyProps = BasePropertyProps & {
type EditPropertyPropsInArray (line 125) | type EditPropertyPropsInArray = EditPropertyProps & {
type ShowPropertyProps (line 137) | type ShowPropertyProps = {
type OnPropertyChange (line 157) | type OnPropertyChange = (
FILE: src/frontend/components/property-type/currency/currency-input-wrapper.tsx
type CurrencyInputWrapperProps (line 6) | type CurrencyInputWrapperProps = {
FILE: src/frontend/components/property-type/default-type/edit.tsx
type CombinedProps (line 11) | type CombinedProps = EditPropertyProps
FILE: src/frontend/components/property-type/index.tsx
type BasePropertyComponentType (line 14) | type BasePropertyComponentType = React.FC<BasePropertyComponentProps> & {
function camelizePropertyType (line 24) | function camelizePropertyType<T>(type: { [key: string]: T }): { [key: st...
FILE: src/frontend/components/property-type/key-value/edit.tsx
type EditKeyValuePairProps (line 11) | type EditKeyValuePairProps = {
FILE: src/frontend/components/property-type/key-value/show.tsx
type ShowKeyValuePairProps (line 8) | type ShowKeyValuePairProps = {
FILE: src/frontend/components/property-type/mixed/convert-to-sub-property.ts
function convertToSubProperty (line 4) | function convertToSubProperty(
FILE: src/frontend/components/property-type/mixed/edit.tsx
type Props (line 10) | type Props = {
FILE: src/frontend/components/property-type/mixed/list.tsx
type ItemComponentProps (line 10) | type ItemComponentProps = BasePropertyProps;
type Props (line 12) | interface Props extends Record<string, unknown> {
FILE: src/frontend/components/property-type/mixed/show.tsx
type Props (line 9) | interface Props extends Record<string, unknown> {
FILE: src/frontend/components/property-type/reference/edit.tsx
type CombinedProps (line 12) | type CombinedProps = EditPropertyProps
type SelectRecordEnhanced (line 13) | type SelectRecordEnhanced = SelectRecord & {
FILE: src/frontend/components/property-type/reference/filter.tsx
type SelectOptions (line 9) | type SelectOptions = Array<{ value: string | number; label: string }>
FILE: src/frontend/components/property-type/reference/reference-value.tsx
type Props (line 16) | type Props = Pick<ShowPropertyProps, 'property' | 'record'>
FILE: src/frontend/components/property-type/richtext/show.tsx
type InnerHtmlProp (line 9) | type InnerHtmlProp = {
FILE: src/frontend/components/property-type/utils/property-description/property-description.tsx
type PropertyDescriptionProps (line 8) | type PropertyDescriptionProps = {
FILE: src/frontend/components/property-type/utils/property-label/property-label.tsx
type PropertyLabelProps (line 9) | type PropertyLabelProps = {
FILE: src/frontend/components/routes/bulk-action.tsx
type MatchParams (line 22) | type MatchParams = Pick<BulkActionParams, 'actionName' | 'resourceId'>
FILE: src/frontend/components/routes/dashboard.tsx
type State (line 13) | type State = {
type PropsFromState (line 17) | type PropsFromState = {
class Dashboard (line 23) | class Dashboard extends React.Component<PropsFromState, State> {
method constructor (line 24) | constructor(props: PropsFromState) {
method componentDidMount (line 31) | componentDidMount(): void {
method render (line 35) | render(): ReactNode {
FILE: src/frontend/components/routes/page.tsx
type PageRouteProps (line 17) | type PageRouteProps = {
FILE: src/frontend/components/routes/resource-action.tsx
type PropsFromState (line 18) | type PropsFromState = {
type Props (line 22) | type Props = PropsFromState
FILE: src/frontend/components/routes/resource.tsx
type PropsFromState (line 16) | type PropsFromState = {
type Props (line 20) | type Props = PropsFromState
type StringifiedBulk (line 22) | type StringifiedBulk<T> = Omit<T, 'recordsId'> & {
FILE: src/frontend/components/routes/utils/should-action-re-fetch-data.ts
type AnyActionParams (line 3) | type AnyActionParams = RecordActionParams & ResourceActionParams & BulkA...
FILE: src/frontend/components/routes/utils/wrapper.tsx
type WrapperProps (line 37) | type WrapperProps = BoxProps & {
FILE: src/frontend/components/spec/test-context-provider.tsx
type Props (line 13) | type Props = {
FILE: src/frontend/hoc/allow-override.tsx
function allowOverride (line 17) | function allowOverride<P extends Record<string, unknown>>(
FILE: src/frontend/hoc/with-notice.ts
type AddNoticeProps (line 12) | type AddNoticeProps = {
FILE: src/frontend/hooks/use-action/use-action.ts
function useAction (line 22) | function useAction<K extends ActionResponse>(
FILE: src/frontend/hooks/use-action/use-action.types.ts
type DifferentActionParams (line 11) | type DifferentActionParams = {
type MergedActionParams (line 18) | type MergedActionParams = RecordActionParams & BulkActionParams & Resour...
type ActionCallCallback (line 20) | type ActionCallCallback = (action: ActionResponse) => any;
type UseActionResultCallApi (line 21) | type UseActionResultCallApi<K extends ActionResponse> = () => Promise<Ax...
type UseActionResult (line 29) | type UseActionResult<K extends ActionResponse> = {
FILE: src/frontend/hooks/use-current-admin.ts
type UseCurrentAdminResponse (line 7) | type UseCurrentAdminResponse = [
function useCurrentAdmin (line 33) | function useCurrentAdmin(): UseCurrentAdminResponse {
FILE: src/frontend/hooks/use-local-storage/use-local-storage-result.type.ts
type UseLocalStorageResult (line 3) | type UseLocalStorageResult<T> = [
FILE: src/frontend/hooks/use-local-storage/use-local-storage.ts
function useLocalStorage (line 18) | function useLocalStorage<T>(key: string, initialValue: T): UseLocalStora...
FILE: src/frontend/hooks/use-navigation-resources.ts
function useNavigationResources (line 19) | function useNavigationResources(
FILE: src/frontend/hooks/use-notice.ts
type AddNotice (line 10) | type AddNotice = (notice: NoticeMessage) => AddNoticeResponse
FILE: src/frontend/hooks/use-query-params.ts
type QueryParams (line 8) | enum QueryParams {
type QueryListParams (line 15) | enum QueryListParams {
type Params (line 23) | type Params<ParamsT = Record<string, unknown>, FiltersT = Record<string,...
function useQueryParams (line 33) | function useQueryParams<
FILE: src/frontend/hooks/use-record/params-to-form-data.ts
constant FORM_VALUE_NULL (line 1) | const FORM_VALUE_NULL = '__FORM_VALUE_NULL__'
constant FORM_VALUE_EMPTY_OBJECT (line 2) | const FORM_VALUE_EMPTY_OBJECT = '__FORM_VALUE_EMPTY_OBJECT__'
constant FORM_VALUE_EMPTY_ARRAY (line 3) | const FORM_VALUE_EMPTY_ARRAY = '__FORM_VALUE_EMPTY_ARRAY__'
function paramsToFormData (line 23) | function paramsToFormData(params: Record<string, any>): FormData {
FILE: src/frontend/hooks/use-record/use-record.type.ts
type UseRecordOptions (line 23) | type UseRecordOptions = {
type UseRecordSubmitFunction (line 45) | type UseRecordSubmitFunction = (
type UseRecordResult (line 64) | type UseRecordResult = {
FILE: src/frontend/hooks/use-records/use-records-result.type.ts
type UseRecordsResult (line 12) | type UseRecordsResult = {
FILE: src/frontend/hooks/use-records/use-records.ts
function useRecords (line 24) | function useRecords(resourceId: string): UseRecordsResult {
FILE: src/frontend/hooks/use-selected-records/use-selected-records-result.type.ts
type UseSelectedRecordsResult (line 9) | type UseSelectedRecordsResult = {
FILE: src/frontend/hooks/use-selected-records/use-selected-records.ts
function useSelectedRecords (line 16) | function useSelectedRecords(records: Array<RecordJSON>): UseSelectedReco...
FILE: src/frontend/hooks/use-translation.ts
type UseTranslationResponse (line 31) | type UseTranslationResponse = TranslateFunctions & {
FILE: src/frontend/interfaces/action/action-json.interface.ts
type ActionJSON (line 10) | interface ActionJSON {
FILE: src/frontend/interfaces/action/build-action-api-call-trigger.ts
type CallApiFunction (line 9) | type CallApiFunction<K extends ActionResponse> = () => Promise<AxiosResp...
type BuildActionCallApiTriggerOptions (line 11) | type BuildActionCallApiTriggerOptions = {
FILE: src/frontend/interfaces/action/build-action-click-handler.ts
type BuildActionClickOptions (line 15) | type BuildActionClickOptions = {
type BuildActionClickReturn (line 25) | type BuildActionClickReturn = (event: any) => any | Promise<any>
FILE: src/frontend/interfaces/action/call-action-api.ts
function callActionApi (line 10) | function callActionApi<K extends ActionResponse>(
FILE: src/frontend/interfaces/modal.interface.ts
type ModalData (line 5) | interface ModalData {
type ModalFunctions (line 12) | type ModalFunctions = {
type ShowModalResponse (line 17) | type ShowModalResponse = {
type HideModalResponse (line 22) | type HideModalResponse = {
FILE: src/frontend/interfaces/noticeMessage.interface.ts
type NoticeMessage (line 9) | type NoticeMessage = {
FILE: src/frontend/interfaces/page-json.interface.ts
type PageJSON (line 7) | interface PageJSON {
FILE: src/frontend/interfaces/property-json/property-json.interface.ts
type PropertyPlace (line 3) | type PropertyPlace = 'show' | 'list' | 'edit' | 'filter';
type PropertyJSON (line 9) | interface PropertyJSON {
type BasePropertyJSON (line 156) | type BasePropertyJSON = Omit<PropertyJSON, 'path'>
FILE: src/frontend/interfaces/record-json.interface.ts
type ErrorMessage (line 9) | type ErrorMessage = {
type RecordJSON (line 20) | interface RecordJSON {
FILE: src/frontend/interfaces/resource-json.interface.ts
type ResourceJSON (line 9) | interface ResourceJSON {
FILE: src/frontend/login-template.tsx
type LoginTemplateAttributes (line 14) | type LoginTemplateAttributes = {
FILE: src/frontend/store/actions/add-notice.ts
constant ADD_NOTICE (line 4) | const ADD_NOTICE = 'ADD_NOTICE'
type AddNoticeResponse (line 6) | type AddNoticeResponse = {
FILE: src/frontend/store/actions/drop-notice.ts
constant DROP_NOTICE (line 1) | const DROP_NOTICE = 'DROP_NOTICE'
type DropNoticeResponse (line 3) | type DropNoticeResponse = {
FILE: src/frontend/store/actions/filter-drawer.ts
constant OPEN_FILTER_DRAWER (line 1) | const OPEN_FILTER_DRAWER = 'OPEN_FILTER_DRAWER'
constant CLOSE_FILTER_DRAWER (line 2) | const CLOSE_FILTER_DRAWER = 'CLOSE_FILTER_DRAWER'
type FilterDrawerAction (line 4) | type FilterDrawerAction =
FILE: src/frontend/store/actions/initialize-assets.ts
constant ASSETS_INITIALIZE (line 3) | const ASSETS_INITIALIZE = 'ASSETS_INITIALIZE'
type initializeAssetsResponse (line 5) | type initializeAssetsResponse = {
FILE: src/frontend/store/actions/initialize-branding.ts
constant BRANDING_INITIALIZE (line 3) | const BRANDING_INITIALIZE = 'BRANDING_INITIALIZE'
type InitializeBrandingResponse (line 5) | type InitializeBrandingResponse = {
FILE: src/frontend/store/actions/initialize-dashboard.ts
constant DASHBOARD_INITIALIZE (line 3) | const DASHBOARD_INITIALIZE = 'DASHBOARD_INITIALIZE'
type InitializeDashboardResponse (line 5) | type InitializeDashboardResponse = {
FILE: src/frontend/store/actions/initialize-locale.ts
constant LOCALE_INITIALIZE (line 3) | const LOCALE_INITIALIZE = 'LOCALE_INITIALIZE'
type InitializeLocaleResponse (line 5) | type InitializeLocaleResponse = {
FILE: src/frontend/store/actions/initialize-pages.ts
constant PAGES_INITIALIZE (line 3) | const PAGES_INITIALIZE = 'PAGES_INITIALIZE'
type InitializePagesResponse (line 5) | type InitializePagesResponse = {
FILE: src/frontend/store/actions/initialize-paths.ts
constant PATHS_INITIALIZE (line 3) | const PATHS_INITIALIZE = 'PATHS_INITIALIZE'
type InitializePathsResponse (line 5) | type InitializePathsResponse = {
FILE: src/frontend/store/actions/initialize-resources.ts
constant RESOURCES_INITIALIZE (line 3) | const RESOURCES_INITIALIZE = 'RESOURCES_INITIALIZE'
type InitializeResourcesResponse (line 5) | type InitializeResourcesResponse = {
FILE: src/frontend/store/actions/initialize-theme.ts
constant THEME_INITIALIZE (line 3) | const THEME_INITIALIZE = 'THEME_INITIALIZE'
type initializeThemeResponse (line 5) | type initializeThemeResponse = {
FILE: src/frontend/store/actions/initialize-versions.ts
constant VERSIONS_INITIALIZE (line 3) | const VERSIONS_INITIALIZE = 'VERSIONS_INITIALIZE'
type InitializeVersionsResponse (line 5) | type InitializeVersionsResponse = {
FILE: src/frontend/store/actions/modal.ts
constant SHOW_MODAL (line 3) | const SHOW_MODAL = 'SHOW_MODAL'
constant HIDE_MODAL (line 5) | const HIDE_MODAL = 'HIDE_MODAL'
FILE: src/frontend/store/actions/route-changed.ts
constant INITIAL_ROUTE (line 3) | const INITIAL_ROUTE = 'INITIAL_ROUTE'
constant ROUTE_CHANGED (line 4) | const ROUTE_CHANGED = 'ROUTE_CHANGED'
type RouteChangedResponse (line 6) | type RouteChangedResponse = {
FILE: src/frontend/store/actions/set-current-admin.ts
constant SESSION_INITIALIZE (line 3) | const SESSION_INITIALIZE = 'SESSION_INITIALIZE'
type SetCurrentAdminResponse (line 5) | type SetCurrentAdminResponse = {
FILE: src/frontend/store/actions/set-drawer-preroute.ts
constant DRAWER_PREROUTE_SET (line 3) | const DRAWER_PREROUTE_SET = 'DRAWER_PREROUTE_SET'
type SetDrawerPreRouteResponse (line 5) | type SetDrawerPreRouteResponse = {
FILE: src/frontend/store/actions/set-notice-progress.ts
constant SET_NOTICE_PROGRESS (line 1) | const SET_NOTICE_PROGRESS = 'SET_NOTICE_PROGRESS'
type SetNoticeProgress (line 3) | type SetNoticeProgress = {
type SetNoticeProgressResponse (line 8) | type SetNoticeProgressResponse = {
FILE: src/frontend/store/reducers/dashboardReducer.ts
type DashboardInState (line 3) | type DashboardInState = {
FILE: src/frontend/store/reducers/drawerReducer.ts
type DrawerInState (line 3) | type DrawerInState = SetDrawerPreRouteResponse['data']
FILE: src/frontend/store/reducers/filterDrawerReducer.ts
type FilterDrawerInState (line 7) | type FilterDrawerInState = ReturnType<typeof filterDrawerReducer>
FILE: src/frontend/store/reducers/localesReducer.ts
type LolcaleInState (line 4) | type LolcaleInState = Locale
FILE: src/frontend/store/reducers/modalReducer.ts
type ModalInState (line 4) | type ModalInState = (ModalData & { show: true }) | { show: false }
FILE: src/frontend/store/reducers/noticesReducer.ts
type NoticeMessageInState (line 6) | interface NoticeMessageInState extends NoticeMessage {
type NoticesInState (line 11) | type NoticesInState = Array<NoticeMessageInState>
type NoticeActionResponse (line 12) | type NoticeActionResponse = AddNoticeResponse | DropNoticeResponse | Set...
FILE: src/frontend/store/reducers/pagesReducer.ts
type PagesInState (line 4) | type PagesInState = Array<PageJSON>
FILE: src/frontend/store/reducers/pathsReducer.ts
type PathsInState (line 4) | type PathsInState = {
FILE: src/frontend/store/reducers/resourcesReducer.ts
type ResourcesInState (line 4) | type ResourcesInState = Array<ResourceJSON>
FILE: src/frontend/store/reducers/routerReducer.ts
type RouterInState (line 4) | type RouterInState = {
FILE: src/frontend/store/reducers/sessionReducer.ts
type SessionInState (line 4) | type SessionInState = CurrentAdmin | null
FILE: src/frontend/store/reducers/themeReducer.ts
type ThemeInState (line 4) | type ThemeInState = (ThemeConfig & { availableThemes?: ThemeConfig[] }) ...
FILE: src/frontend/store/store.ts
type ReduxState (line 36) | type ReduxState = {
FILE: src/frontend/utils/api-client.ts
type ActionAPIParams (line 57) | type ActionAPIParams = AxiosRequestConfig & ActionParams
type ResourceActionAPIParams (line 66) | type ResourceActionAPIParams = AxiosRequestConfig & ResourceActionParams...
type RecordActionAPIParams (line 76) | type RecordActionAPIParams = AxiosRequestConfig & RecordActionParams
type BulkActionAPIParams (line 86) | type BulkActionAPIParams = AxiosRequestConfig & BulkActionParams
type GetPageAPIParams (line 95) | type GetPageAPIParams = AxiosRequestConfig & {
class ApiClient (line 120) | class ApiClient {
method constructor (line 125) | constructor() {
method getBaseUrl (line 132) | static getBaseUrl(): string {
method searchRecords (line 147) | async searchRecords({ resourceId, query, searchProperty }: {
method resourceAction (line 170) | async resourceAction(options: ResourceActionAPIParams): Promise<AxiosR...
method recordAction (line 193) | async recordAction(options: RecordActionAPIParams): Promise<AxiosRespo...
method bulkAction (line 211) | async bulkAction(options: BulkActionAPIParams): Promise<AxiosResponse<...
method getDashboard (line 235) | async getDashboard<T = unknown>(options: AxiosRequestConfig = {}): Pro...
method getPage (line 248) | async getPage<T = unknown>(options: GetPageAPIParams): Promise<AxiosRe...
method refreshToken (line 258) | async refreshToken(data: Record<string, any>) {
FILE: src/frontend/utils/overridable-component.ts
type OverridableComponent (line 1) | type OverridableComponent =
FILE: src/locale/config.ts
type AdminJSLocalesConfig (line 3) | type AdminJSLocalesConfig = InitOptions & {
type Locale (line 24) | type Locale = AdminJSLocalesConfig & {
type AdminJSLocaleNamespaces (line 37) | type AdminJSLocaleNamespaces =
type LocaleTranslationsBlock (line 46) | type LocaleTranslationsBlock = ResourceLanguage &
type LocaleTranslations (line 50) | type LocaleTranslations = Partial<LocaleTranslationsBlock> & {
FILE: src/locale/default-config.ts
constant DEFAULT_LOAD (line 6) | const DEFAULT_LOAD = 'currentOnly'
constant DEFAULT_NS (line 7) | const DEFAULT_NS = 'translation'
method initImmediate (line 30) | get initImmediate(): boolean {
FILE: src/utils/error-type.enum.ts
type ErrorTypeEnum (line 2) | enum ErrorTypeEnum {
FILE: src/utils/flat/constants.ts
constant DELIMITER (line 1) | const DELIMITER = '.'
FILE: src/utils/flat/flat-module.ts
type FlatModuleType (line 12) | type FlatModuleType = {
FILE: src/utils/flat/flat.types.ts
type FlattenParams (line 7) | type FlattenParams = {
type FlattenValue (line 11) | type FlattenValue = string
type GetOptions (line 24) | type GetOptions = {
FILE: src/utils/flat/get.ts
constant TEMP_HOLDING_KEY (line 8) | const TEMP_HOLDING_KEY = 'TEMP_HOLDING_KEY'
FILE: src/utils/flat/path-parts.type.ts
type PathParts (line 1) | type PathParts = Array<string>
FILE: src/utils/flat/path-to-parts.ts
type PathToPartsOptions (line 7) | type PathToPartsOptions = {
FILE: src/utils/param-converter/constants.ts
constant DELIMITER (line 1) | const DELIMITER = '.'
FILE: src/utils/param-converter/param-converter-module.ts
type ParamConverterModuleType (line 6) | type ParamConverterModuleType = {
FILE: src/utils/translate-functions.factory.ts
type TranslateFunction (line 8) | type TranslateFunction = (
type TranslateFunctions (line 30) | interface TranslateFunctions {
FILE: vendor-types/chai/index.d.ts
type AlterOptions (line 2) | type AlterOptions = {
type Assertion (line 8) | interface Assertion {
FILE: vendor-types/global.ts
type Window (line 8) | interface Window {
FILE: vendor-types/node/node.d.ts
type UserComponentsMap (line 1) | type UserComponentsMap = {[key: string]: string}
type Global (line 4) | interface Global {
Condensed preview — 494 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,154K chars).
[
{
"path": ".babelrc.json",
"chars": 636,
"preview": "{\n \"presets\": [\n \"@babel/preset-react\",\n [\"@babel/preset-env\", {\n \"targets\": {\n \"node\": \"18\"\n },"
},
{
"path": ".cspell.json",
"chars": 753,
"preview": "{\n \"version\": \"0.1\",\n \"language\": \"en\",\n \"words\": [\n \"crossorigin\", \"nullish\", \"envs\", \"prefetch\", \"expr"
},
{
"path": ".dockerignore",
"chars": 26,
"preview": "node_modules\nnpm-debug.log"
},
{
"path": ".eslintrc.cjs",
"chars": 3062,
"preview": "module.exports = {\n parser: '@typescript-eslint/parser',\n plugins: ['@typescript-eslint', 'mocha'],\n env: {\n es6: "
},
{
"path": ".gitattributes",
"chars": 18,
"preview": "* text=auto eol=lf"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 2165,
"preview": "name: Bug Report\ndescription: File a bug report\ntitle: \"[Bug]: \"\nlabels: [\"bug\", \"triage\"]\nassignees:\n - octocat\nbody:\n"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1557,
"preview": "---\nname: 🛠️ Feature Request\ndescription: Suggest an idea to help us improve AdminJS\ntitle: \"[Feature]: \"\nlabels:\n - \"f"
},
{
"path": ".github/workflows/push.yml",
"chars": 4682,
"preview": "name: CI/CD\non: [push, pull_request]\n\njobs:\n setup:\n name: setup\n runs-on: ubuntu-latest\n steps:\n - name:"
},
{
"path": ".gitignore",
"chars": 282,
"preview": ".nyc_output\n.DS_store\n.vscode\n.nova\n.idea\n.cache\nlib\nbuilt\ntypes\n\ncoverage\n\ndocker-compose.ovverride.yml\n\nnode_modules\n\n"
},
{
"path": ".npmignore",
"chars": 118,
"preview": ".nyc_output\n.DS_store\n.vscode\n.idea\n.travis.yml\n\n/coverage\n\ndocker-compose.ovverride.yml\n\nnode_modules\n\ndocs\n/packages"
},
{
"path": ".npmrc",
"chars": 13,
"preview": "access=public"
},
{
"path": ".prettierrc",
"chars": 332,
"preview": "{\n \"printWidth\": 100,\n \"singleQuote\": true,\n \"trailingComma\": \"all\",\n \"semi\": false,\n \"overrides\": [\n {\n \"f"
},
{
"path": ".releaserc",
"chars": 706,
"preview": "{\n \"branches\": [\n \"+([0-9])?(.{+([0-9]),x}).x\",\n \"master\",\n {\n \"name\": \"beta\",\n \"prerelease\": true\n "
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 4043,
"preview": "# Contributor Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as cont"
},
{
"path": "CONTRIBUTING.md",
"chars": 8617,
"preview": "# AdminJS Development\n\n## Explanation of AdminJS libraries\nThere are three main terms we use to define AdminJS libraries"
},
{
"path": "LICENSE.md",
"chars": 1058,
"preview": "Copyright 2018 SoftwareBrothers.co\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this"
},
{
"path": "README.md",
"chars": 3030,
"preview": "# AdminJS\n\n[AdminJS](https://adminjs.co/) is an automatic admin interface that can be plugged into your application. You"
},
{
"path": "UPGRADE-6.0.md",
"chars": 2055,
"preview": "# Migration guide to version v6\n\n## Updating AdminJS to v6\n\nTo update AdminJS package to the sixth wersion please use fo"
},
{
"path": "bin/app.js",
"chars": 83,
"preview": "import bundler from '../lib/backend/bundler/app.bundler.js'\n\nawait bundler.build()\n"
},
{
"path": "bin/globals.js",
"chars": 87,
"preview": "import bundler from '../lib/backend/bundler/globals.bundler.js'\n\nawait bundler.build()\n"
},
{
"path": "cli.js",
"chars": 68,
"preview": "#!/usr/bin/env node\n// eslint-disable-next-line\nimport('./lib/cli')\n"
},
{
"path": "commitlint.config.cjs",
"chars": 78,
"preview": "module.exports = {\n extends: [\n '@commitlint/config-conventional',\n ],\n}\n"
},
{
"path": "cy/commands/ab-get-property.js",
"chars": 1148,
"preview": "/// <reference types=\"cypress\" />\n\n/**\n * @method abGetProperty\n * @description\n * ### Usage\n *\n * ```javascript\n * // y"
},
{
"path": "cy/commands/ab-keep-logged-in.js",
"chars": 520,
"preview": "/// <reference types=\"cypress\" />\n\n/**\n * @method abKeepLoggedIn\n * @memberof module:cy\n * @param {object} [options]\n * "
},
{
"path": "cy/commands/ab-login-api.js",
"chars": 871,
"preview": "/// <reference types=\"cypress\" />\n\n/**\n * @method abLoginAPI\n * @memberof module:cy\n * @description\n * Comparing to {@li"
},
{
"path": "cy/commands/ab-login.js",
"chars": 840,
"preview": "/// <reference types=\"cypress\" />\n\n/**\n * @method abLogin\n * @description\n * logs you to the AdminJS. Since the system u"
},
{
"path": "cy/cypress.doc.md",
"chars": 947,
"preview": "### Cypress helpers\n\nThis module gathers helpers which can be used when you E2E test your AdminJS dashboard with\n{@link "
},
{
"path": "cy/index.d.ts",
"chars": 509,
"preview": "/// <reference types=\"cypress\" />\n\ndeclare namespace Cypress {\n type AbLoginParams = {\n email?: string;\n password"
},
{
"path": "cy/index.js",
"chars": 197,
"preview": "/**\n * @module cy\n * @load ./cypress.doc.md\n */\n\nimport './commands/ab-login.js'\nimport './commands/ab-login-api.js'\nimp"
},
{
"path": "cy/readme.md",
"chars": 12874,
"preview": "## Test suite: Login page form\n\n### LPF-1: Log in to the app with a valid email and password\n\n**Test description:** Veri"
},
{
"path": "index.d.ts",
"chars": 247,
"preview": "import AdminJS from './types/src/index.js'\n\nimport { ReduxState } from './types/src/frontend/store/store.js'\n\nexport * f"
},
{
"path": "index.js",
"chars": 116,
"preview": "import AdminJS from './lib/index.js'\n\nexport * from './lib/index.js'\n\nexport {\n AdminJS,\n}\n\nexport default AdminJS\n"
},
{
"path": "package.json",
"chars": 5453,
"preview": "{\n \"name\": \"adminjs\",\n \"version\": \"7.8.17\",\n \"description\": \"Admin panel for apps written in node.js\",\n \"type\": \"mod"
},
{
"path": "project.inlang/project_id",
"chars": 64,
"preview": "ed92cfe9d9d26d48639c434e4f5ddd645dfbd5ecaac14329f8b042b5546fd3a9"
},
{
"path": "project.inlang/settings.json",
"chars": 796,
"preview": "{\n \"$schema\": \"https://inlang.com/schema/project-settings\",\n \"sourceLanguageTag\": \"en\",\n \"languageTags\": [\n "
},
{
"path": "spec/backend/helpers/helper-stub.ts",
"chars": 1787,
"preview": "import sinon from 'sinon'\n\nimport ViewHelpers from '../../../src/backend/utils/view-helpers/view-helpers.js'\n\nexport con"
},
{
"path": "spec/backend/helpers/resource-stub.ts",
"chars": 2571,
"preview": "import sinon from 'sinon'\n\nimport BaseProperty from '../../../src/backend/adapters/property/base-property.js'\nimport Bas"
},
{
"path": "spec/backend/index.js",
"chars": 434,
"preview": "/* eslint-disable import/first */\nimport { factory } from 'factory-girl'\n\nprocess.env.MONGO_URL = 'mongodb://mongo/admin"
},
{
"path": "spec/fixtures/action.factory.js",
"chars": 457,
"preview": "import { factory } from 'factory-girl'\n\nfactory.define('action', Object, {\n name: factory.sequence('action.name', (n) ="
},
{
"path": "spec/fixtures/example-component.js",
"chars": 130,
"preview": "import React from 'react'\n\nfunction ExampleComponent() {\n return <div>Some example text</div>\n}\n\nexport default Example"
},
{
"path": "spec/fixtures/property.factory.js",
"chars": 355,
"preview": "import { factory } from 'factory-girl'\n\nfactory.define('property', Object, {\n isId: false,\n isSortable: true,\n isTitl"
},
{
"path": "spec/fixtures/record.factory.js",
"chars": 248,
"preview": "import { factory } from 'factory-girl'\n\nfactory.define('record', Object, {\n params: {\n param1: 'value',\n 'nested."
},
{
"path": "spec/index.js",
"chars": 909,
"preview": "/* eslint-disable import/no-extraneous-dependencies */\n/* eslint-disable import/first */\n/* eslint-disable @typescript-e"
},
{
"path": "spec/lib.js",
"chars": 244,
"preview": "/* eslint-disable @typescript-eslint/no-var-requires */\n/* eslint-disable func-names */\nimport { importAll } from 'node-"
},
{
"path": "spec/setup.js",
"chars": 497,
"preview": "/* eslint-disable func-names */\n/* eslint-disable mocha/no-top-level-hooks */\nimport { URLSearchParams } from 'url'\n\nimp"
},
{
"path": "src/adminjs-options.interface.ts",
"chars": 12204,
"preview": "import { ThemeOverride } from '@adminjs/design-system'\nimport type { TransformOptions as BabelConfig } from 'babel-core'"
},
{
"path": "src/adminjs.spec.ts",
"chars": 4497,
"preview": "import path from 'path'\nimport { expect } from 'chai'\nimport * as url from 'url'\n\nimport AdminJS from './adminjs.js'\nimp"
},
{
"path": "src/adminjs.ts",
"chars": 10382,
"preview": "import merge from 'lodash/merge.js'\nimport * as path from 'path'\nimport * as fs from 'fs'\nimport * as url from 'url'\n\nim"
},
{
"path": "src/babel.test.config.json",
"chars": 592,
"preview": "{\n \"presets\": [\n \"@babel/preset-react\",\n [\n \"@babel/preset-env\",\n {\n \"targets\": {\n \"nod"
},
{
"path": "src/backend/actions/action.interface.ts",
"chars": 18303,
"preview": "import { IconProps, VariantType } from '@adminjs/design-system'\n\nimport AdminJS from '../../adminjs.js'\nimport { Current"
},
{
"path": "src/backend/actions/bulk-delete/bulk-delete-action.spec.ts",
"chars": 3186,
"preview": "import chai, { expect } from 'chai'\nimport chaiAsPromised from 'chai-as-promised'\nimport sinon from 'sinon'\n\nimport Bulk"
},
{
"path": "src/backend/actions/bulk-delete/bulk-delete-action.ts",
"chars": 1831,
"preview": "import { Action, BulkActionResponse } from '../action.interface.js'\nimport NotFoundError from '../../utils/errors/not-fo"
},
{
"path": "src/backend/actions/delete/delete-action.spec.ts",
"chars": 4076,
"preview": "import chai, { expect } from 'chai'\nimport chaiAsPromised from 'chai-as-promised'\nimport sinon from 'sinon'\n\nimport Dele"
},
{
"path": "src/backend/actions/delete/delete-action.ts",
"chars": 2031,
"preview": "import { Action, RecordActionResponse } from '../action.interface.js'\nimport NotFoundError from '../../utils/errors/not-"
},
{
"path": "src/backend/actions/edit/edit-action.ts",
"chars": 2150,
"preview": "import { Action, RecordActionResponse } from '../action.interface.js'\nimport NotFoundError from '../../utils/errors/not-"
},
{
"path": "src/backend/actions/index.ts",
"chars": 984,
"preview": "import { DeleteAction } from './delete/delete-action.js'\nimport { ShowAction } from './show/show-action.js'\nimport { New"
},
{
"path": "src/backend/actions/list/list-action.ts",
"chars": 2903,
"preview": "import { flat } from '../../../utils/flat/index.js'\nimport { Action, ActionQueryParameters, ActionResponse } from '../ac"
},
{
"path": "src/backend/actions/new/new-action.ts",
"chars": 2032,
"preview": "import { populator } from '../../utils/populator/index.js'\nimport { paramConverter } from '../../../utils/param-converte"
},
{
"path": "src/backend/actions/search/search-action.ts",
"chars": 2273,
"preview": "import { flat } from '../../../utils/flat/index.js'\nimport { Action, ActionResponse, ActionQueryParameters } from '../ac"
},
{
"path": "src/backend/actions/show/show-action.ts",
"chars": 1076,
"preview": "import { Action, RecordActionResponse } from '../action.interface.js'\nimport NotFoundError from '../../utils/errors/not-"
},
{
"path": "src/backend/adapters/database/base-database.ts",
"chars": 1250,
"preview": "/* eslint-disable no-useless-constructor */\n/* eslint-disable @typescript-eslint/no-unused-vars */\n/* eslint-disable @ty"
},
{
"path": "src/backend/adapters/database/index.ts",
"chars": 61,
"preview": "export { default as BaseDatabase } from './base-database.js'\n"
},
{
"path": "src/backend/adapters/index.ts",
"chars": 142,
"preview": "export * from './database/index.js'\nexport * from './property/index.js'\nexport * from './record/index.js'\nexport * from "
},
{
"path": "src/backend/adapters/property/base-property.ts",
"chars": 4972,
"preview": "/* eslint class-methods-use-this: 0 object-curly-newline: 0 */\n\n/**\n * @name PropertyType\n * @typedef {object} PropertyT"
},
{
"path": "src/backend/adapters/property/index.ts",
"chars": 116,
"preview": "export { default as BaseProperty } from './base-property.js'\nexport type { PropertyType } from './base-property.js'\n"
},
{
"path": "src/backend/adapters/record/base-record.spec.ts",
"chars": 9938,
"preview": "import chai, { expect } from 'chai'\nimport chaiChange from 'chai-change'\nimport sinon from 'sinon'\nimport sinonChai from"
},
{
"path": "src/backend/adapters/record/base-record.ts",
"chars": 10017,
"preview": "import { flat, GetOptions } from '../../../utils/flat/index.js'\nimport { ParamsType } from './params.type.js'\nimport Bas"
},
{
"path": "src/backend/adapters/record/index.ts",
"chars": 90,
"preview": "export { default as BaseRecord } from './base-record.js'\nexport * from './params.type.js'\n"
},
{
"path": "src/backend/adapters/record/params.type.ts",
"chars": 326,
"preview": "/**\n * @alias ParamsTypeValue\n * @memberof BaseRecord\n */\nexport type ParamsTypeValue = string\n | number\n | boolean\n "
},
{
"path": "src/backend/adapters/resource/base-resource.spec.ts",
"chars": 3569,
"preview": "import chai, { expect } from 'chai'\nimport sinon from 'sinon'\nimport chaiAsPromised from 'chai-as-promised'\n\nimport Base"
},
{
"path": "src/backend/adapters/resource/base-resource.ts",
"chars": 9007,
"preview": "/* eslint-disable @typescript-eslint/no-empty-function */\n/* eslint-disable @typescript-eslint/no-unused-vars */\n/* esli"
},
{
"path": "src/backend/adapters/resource/index.ts",
"chars": 107,
"preview": "export { default as BaseResource } from './base-resource.js'\nexport * from './supported-databases.type.js'\n"
},
{
"path": "src/backend/adapters/resource/supported-databases.type.ts",
"chars": 183,
"preview": "export type SupportedDatabasesType = 'MySQL'\n | 'MariaDB'\n | 'Postgres'\n | 'CockroachDB'\n | 'SQLite'\n | 'MicrosoftS"
},
{
"path": "src/backend/bundler/app.bundler.ts",
"chars": 1540,
"preview": "import path from 'path'\nimport * as url from 'url'\n\nimport { InputOptions, OutputOptions } from 'rollup'\nimport { nodeRe"
},
{
"path": "src/backend/bundler/components.bundler.ts",
"chars": 1992,
"preview": "import { InputOptions, OutputOptions } from 'rollup'\nimport { nodeResolve as resolve } from '@rollup/plugin-node-resolve"
},
{
"path": "src/backend/bundler/generate-user-component-entry.spec.js",
"chars": 1662,
"preview": "import path from 'path'\nimport * as url from 'url'\n\nimport AdminJS from '../../adminjs.js'\nimport { ComponentLoader } fr"
},
{
"path": "src/backend/bundler/generate-user-component-entry.ts",
"chars": 1481,
"preview": "import * as path from 'path'\nimport slash from 'slash'\n\nimport AdminJS from '../../adminjs.js'\n\n/**\n * Generates entry f"
},
{
"path": "src/backend/bundler/globals.bundler.ts",
"chars": 1856,
"preview": "import path from 'path'\nimport * as url from 'url'\n\nimport { InputOptions, OutputOptions } from 'rollup'\nimport { nodeRe"
},
{
"path": "src/backend/bundler/index.ts",
"chars": 377,
"preview": "export { default as appBundler } from './app.bundler.js'\nexport { default as globalsBundler } from './globals.bundler.js"
},
{
"path": "src/backend/bundler/utils/asset-bundler.ts",
"chars": 3977,
"preview": "/* eslint-disable import/no-extraneous-dependencies */\nimport { readFile, mkdir, writeFile } from 'fs/promises'\nimport {"
},
{
"path": "src/backend/bundler/utils/constants.ts",
"chars": 443,
"preview": "import path from 'path'\n\nexport const NODE_ENV = process.env.NODE_ENV === 'production' ? 'production' : 'development'\n\nc"
},
{
"path": "src/backend/controllers/api-controller.spec.js",
"chars": 3598,
"preview": "/* eslint-disable @typescript-eslint/explicit-function-return-type */\nimport { expect } from 'chai'\n\nimport ApiControlle"
},
{
"path": "src/backend/controllers/api-controller.ts",
"chars": 11262,
"preview": "/* eslint-disable max-len */\n/* eslint no-unused-vars: 0 */\nimport populator from '../utils/populator/populator.js'\nimpo"
},
{
"path": "src/backend/controllers/app-controller.ts",
"chars": 2911,
"preview": "/* eslint-disable no-unused-vars */\nimport ViewHelpers from '../utils/view-helpers/view-helpers.js'\nimport componentsBun"
},
{
"path": "src/backend/controllers/index.ts",
"chars": 126,
"preview": "export { default as AppController } from './app-controller.js'\nexport { default as ApiController } from './api-controlle"
},
{
"path": "src/backend/decorators/action/action-decorator.spec.ts",
"chars": 5846,
"preview": "/* eslint-disable @typescript-eslint/explicit-function-return-type */\nimport { expect } from 'chai'\nimport sinon from 's"
},
{
"path": "src/backend/decorators/action/action-decorator.ts",
"chars": 10265,
"preview": "import { DEFAULT_DRAWER_WIDTH, VariantType } from '@adminjs/design-system'\n\nimport ConfigurationError from '../../utils/"
},
{
"path": "src/backend/decorators/action/index.ts",
"chars": 67,
"preview": "export { default as ActionDecorator } from './action-decorator.js'\n"
},
{
"path": "src/backend/decorators/index.ts",
"chars": 106,
"preview": "export * from './action/index.js'\nexport * from './property/index.js'\nexport * from './resource/index.js'\n"
},
{
"path": "src/backend/decorators/property/index.ts",
"chars": 190,
"preview": "export { PropertyDecorator } from './property-decorator.js'\nexport type { default as PropertyOptions } from './property-"
},
{
"path": "src/backend/decorators/property/property-decorator.spec.ts",
"chars": 6908,
"preview": "import { expect } from 'chai'\nimport sinon, { SinonStubbedInstance } from 'sinon'\n\nimport PropertyDecorator from './prop"
},
{
"path": "src/backend/decorators/property/property-decorator.ts",
"chars": 8512,
"preview": "import AdminJS from '../../../adminjs.js'\nimport { BasePropertyJSON, PropertyPlace } from '../../../frontend/interfaces/"
},
{
"path": "src/backend/decorators/property/property-options.interface.ts",
"chars": 3448,
"preview": "import { PropertyType } from '../../adapters/property/base-property.js'\n\n/**\n * Options passed to a given property\n */\ne"
},
{
"path": "src/backend/decorators/property/utils/index.ts",
"chars": 43,
"preview": "export * from './override-from-options.js'\n"
},
{
"path": "src/backend/decorators/property/utils/override-from-options.spec.ts",
"chars": 904,
"preview": "import { expect } from 'chai'\nimport sinon from 'sinon'\n\nimport { PropertyType, BaseProperty } from '../../../adapters/p"
},
{
"path": "src/backend/decorators/property/utils/override-from-options.ts",
"chars": 612,
"preview": "import { BaseProperty } from '../../../adapters/property/index.js'\nimport PropertyOptions from '../property-options.inte"
},
{
"path": "src/backend/decorators/resource/index.ts",
"chars": 119,
"preview": "export { default as ResourceDecorator } from './resource-decorator.js'\nexport * from './resource-options.interface.js'\n"
},
{
"path": "src/backend/decorators/resource/resource-decorator.spec.ts",
"chars": 9756,
"preview": "import sinon from 'sinon'\nimport { expect } from 'chai'\n\nimport ResourceDecorator from './resource-decorator.js'\nimport "
},
{
"path": "src/backend/decorators/resource/resource-decorator.ts",
"chars": 10365,
"preview": "import { DecoratedActions } from './utils/decorate-actions.js'\nimport { BaseResource, BaseRecord } from '../../adapters/"
},
{
"path": "src/backend/decorators/resource/resource-options.interface.ts",
"chars": 4139,
"preview": "import type { IconProps } from '@adminjs/design-system'\n\nimport { Action, ActionResponse, RecordActionResponse, BulkActi"
},
{
"path": "src/backend/decorators/resource/utils/decorate-actions.ts",
"chars": 1691,
"preview": "import mergeWith from 'lodash/mergeWith.js'\n\nimport ResourceDecorator from '../resource-decorator.js'\nimport AdminJS fro"
},
{
"path": "src/backend/decorators/resource/utils/decorate-properties.spec.ts",
"chars": 7190,
"preview": "import { expect } from 'chai'\nimport sinon, { SinonStubbedInstance } from 'sinon'\n\nimport ResourceDecorator from '../res"
},
{
"path": "src/backend/decorators/resource/utils/decorate-properties.ts",
"chars": 3765,
"preview": "import ResourceDecorator from '../resource-decorator.js'\nimport AdminJS from '../../../../adminjs.js'\nimport { BasePrope"
},
{
"path": "src/backend/decorators/resource/utils/find-sub-property.ts",
"chars": 1173,
"preview": "import PropertyDecorator from '../../property/property-decorator.js'\nimport { PathParts } from '../../../../utils/flat/p"
},
{
"path": "src/backend/decorators/resource/utils/flat-sub-properties.ts",
"chars": 813,
"preview": "import PropertyDecorator from '../../property/property-decorator.js'\n\n/**\n * Bu default all subProperties are nested as "
},
{
"path": "src/backend/decorators/resource/utils/get-navigation.spec.ts",
"chars": 2089,
"preview": "import { expect } from 'chai'\n\nimport { ResourceOptions } from '../resource-options.interface.js'\nimport { getNavigation"
},
{
"path": "src/backend/decorators/resource/utils/get-navigation.ts",
"chars": 1660,
"preview": "import { ResourceJSON } from '../../../../frontend/interfaces/index.js'\nimport { ResourceOptions } from '../resource-opt"
},
{
"path": "src/backend/decorators/resource/utils/get-property-by-key.ts",
"chars": 1047,
"preview": "import { PropertyDecorator } from '../../property/index.js'\nimport { DecoratedProperties } from './decorate-properties.j"
},
{
"path": "src/backend/decorators/resource/utils/index.ts",
"chars": 236,
"preview": "export * from './find-sub-property.js'\nexport * from './flat-sub-properties.js'\nexport * from './get-navigation.js'\nexpo"
},
{
"path": "src/backend/index.ts",
"chars": 217,
"preview": "export * from './actions/index.js'\nexport * from './adapters/index.js'\nexport * from './controllers/index.js'\nexport * f"
},
{
"path": "src/backend/services/action-error-handler/action-error-handler.spec.ts",
"chars": 3872,
"preview": "import sinon from 'sinon'\nimport { expect } from 'chai'\n\nimport { ActionContext } from '../../actions/action.interface.j"
},
{
"path": "src/backend/services/action-error-handler/action-error-handler.ts",
"chars": 2338,
"preview": "import { NoticeMessage } from '../../../index.js'\nimport { ActionContext, BulkActionResponse, RecordActionResponse } fro"
},
{
"path": "src/backend/services/action-error-handler/index.ts",
"chars": 74,
"preview": "export { default as actionErrorHandler } from './action-error-handler.js'\n"
},
{
"path": "src/backend/services/index.ts",
"chars": 87,
"preview": "export * from './action-error-handler/index.js'\nexport * from './sort-setter/index.js'\n"
},
{
"path": "src/backend/services/sort-setter/index.ts",
"chars": 57,
"preview": "export { default as SortSetter } from './sort-setter.js'\n"
},
{
"path": "src/backend/services/sort-setter/sort-setter.spec.js",
"chars": 1206,
"preview": "import sortSetter from './sort-setter.js'\n\ndescribe('sortSetter', function () {\n const defaultFieldName = 'someFieldNam"
},
{
"path": "src/backend/services/sort-setter/sort-setter.ts",
"chars": 1616,
"preview": "import ConfigurationError from '../../utils/errors/configuration-error.js'\nimport { ResourceOptions } from '../../decora"
},
{
"path": "src/backend/utils/auth/base-auth-provider.ts",
"chars": 3798,
"preview": "/* eslint-disable @typescript-eslint/no-unused-vars */\n/* eslint-disable max-len */\n/* eslint-disable class-methods-use-"
},
{
"path": "src/backend/utils/auth/default-auth-provider.ts",
"chars": 815,
"preview": "import { AuthProviderConfig, AuthenticatePayload, BaseAuthProvider, LoginHandlerOptions } from './base-auth-provider.js'"
},
{
"path": "src/backend/utils/auth/index.ts",
"chars": 83,
"preview": "export * from './base-auth-provider.js'\nexport * from './default-auth-provider.js'\n"
},
{
"path": "src/backend/utils/build-feature/build-feature.spec.ts",
"chars": 2545,
"preview": "/* eslint-disable @typescript-eslint/no-empty-function */\nimport { expect } from 'chai'\nimport { mergeResourceOptions } "
},
{
"path": "src/backend/utils/build-feature/build-feature.ts",
"chars": 4560,
"preview": "/* eslint-disable no-nested-ternary */\nimport uniq from 'lodash/uniq.js'\nimport merge from 'lodash/merge.js'\n\nimport Adm"
},
{
"path": "src/backend/utils/build-feature/index.ts",
"chars": 35,
"preview": "export * from './build-feature.js'\n"
},
{
"path": "src/backend/utils/component-loader.ts",
"chars": 5088,
"preview": "import * as path from 'path'\nimport * as fs from 'fs'\n\nimport { ConfigurationError } from './errors/index.js'\nimport { r"
},
{
"path": "src/backend/utils/errors/app-error.ts",
"chars": 1313,
"preview": "import { NoticeMessage } from '../../../index.js'\nimport { ErrorTypeEnum } from '../../../utils/error-type.enum.js'\nimpo"
},
{
"path": "src/backend/utils/errors/configuration-error.ts",
"chars": 783,
"preview": "import { ErrorTypeEnum } from '../../../utils/error-type.enum.js'\nimport * as CONSTANTS from '../../../constants.js'\n\nco"
},
{
"path": "src/backend/utils/errors/forbidden-error.ts",
"chars": 954,
"preview": "import { ErrorTypeEnum } from '../../../utils/error-type.enum.js'\nimport RecordError from './record-error.js'\n\n/**\n * Er"
},
{
"path": "src/backend/utils/errors/index.ts",
"chars": 261,
"preview": "export * from './app-error.js'\nexport * from './configuration-error.js'\nexport * from './forbidden-error.js'\nexport * fr"
},
{
"path": "src/backend/utils/errors/not-found-error.ts",
"chars": 1225,
"preview": "import { ErrorTypeEnum } from '../../../utils/error-type.enum.js'\nimport * as CONSTANTS from '../../../constants.js'\nimp"
},
{
"path": "src/backend/utils/errors/not-implemented-error.ts",
"chars": 888,
"preview": "import { DOCS } from '../../../constants.js'\n\nconst buildUrl = (fnName: string): string => {\n if (fnName) {\n let obj"
},
{
"path": "src/backend/utils/errors/record-error.ts",
"chars": 327,
"preview": "import { ErrorTypeEnum } from '../../../utils/error-type.enum.js'\n\n/**\n * Record Error\n * @alias RecordError\n * @membero"
},
{
"path": "src/backend/utils/errors/validation-error.ts",
"chars": 1206,
"preview": "import { ErrorTypeEnum } from '../../../utils/error-type.enum.js'\nimport RecordError from './record-error.js'\n\n/**\n * Pr"
},
{
"path": "src/backend/utils/filter/filter.ts",
"chars": 2877,
"preview": "import { flat } from '../../../utils/flat/index.js'\nimport BaseProperty from '../../adapters/property/base-property.js'\n"
},
{
"path": "src/backend/utils/filter/index.ts",
"chars": 28,
"preview": "export * from './filter.js'\n"
},
{
"path": "src/backend/utils/index.ts",
"chars": 508,
"preview": "export * from './auth/index.js'\nexport * from './build-feature/index.js'\nexport * from './errors/index.js'\nexport * from"
},
{
"path": "src/backend/utils/layout-element-parser/index.ts",
"chars": 43,
"preview": "export * from './layout-element-parser.js'\n"
},
{
"path": "src/backend/utils/layout-element-parser/layout-element-parser.spec.ts",
"chars": 2546,
"preview": "import { expect } from 'chai'\n\nimport layoutElementParser, { LayoutElement } from './layout-element-parser.js'\n\ndescribe"
},
{
"path": "src/backend/utils/layout-element-parser/layout-element-parser.ts",
"chars": 4700,
"preview": "/* eslint-disable max-len */\nimport {\n BoxProps,\n HeaderProps,\n TextProps,\n BadgeProps,\n ButtonProps,\n LinkProps,\n"
},
{
"path": "src/backend/utils/layout-element-parser/layout-element.doc.md",
"chars": 4079,
"preview": "{@link LayoutElement} is used to change the default layout of `edit`, `show` and `new` {@link Action actions}.\nYou defin"
},
{
"path": "src/backend/utils/options-parser/index.ts",
"chars": 36,
"preview": "export * from './options-parser.js'\n"
},
{
"path": "src/backend/utils/options-parser/options-parser.ts",
"chars": 3452,
"preview": "import merge from 'lodash/merge.js'\n\nimport { AdminJSOptions, Assets, BrandingOptions } from '../../../adminjs-options.i"
},
{
"path": "src/backend/utils/populator/index.ts",
"chars": 70,
"preview": "export * from './populator.js'\nexport * from './populate-property.js'\n"
},
{
"path": "src/backend/utils/populator/populate-property.spec.ts",
"chars": 4707,
"preview": "import { expect } from 'chai'\nimport sinon, { SinonStubbedInstance } from 'sinon'\n\nimport { BaseProperty, BaseRecord, Ba"
},
{
"path": "src/backend/utils/populator/populate-property.ts",
"chars": 4221,
"preview": "import { BaseRecord } from '../../adapters/index.js'\nimport PropertyDecorator from '../../decorators/property/property-d"
},
{
"path": "src/backend/utils/populator/populator.doc.md",
"chars": 1172,
"preview": "Populates all references in records.\n\n### Usage\n\nTake a look at an example action handler getting the Product record and"
},
{
"path": "src/backend/utils/populator/populator.spec.ts",
"chars": 319,
"preview": "import { expect } from 'chai'\n\nimport populator from './populator.js'\n\ndescribe('populator', () => {\n context('empty ar"
},
{
"path": "src/backend/utils/populator/populator.ts",
"chars": 860,
"preview": "import BaseRecord from '../../adapters/record/base-record.js'\nimport { populateProperty } from './populate-property.js'\n"
},
{
"path": "src/backend/utils/request-parser/index.ts",
"chars": 36,
"preview": "export * from './request-parser.js'\n"
},
{
"path": "src/backend/utils/request-parser/request-parser.spec.ts",
"chars": 1446,
"preview": "import { expect } from 'chai'\n\nimport requestParser from './request-parser.js'\nimport { ActionRequest } from '../../acti"
},
{
"path": "src/backend/utils/request-parser/request-parser.ts",
"chars": 1932,
"preview": "import { ActionRequest } from '../../actions/index.js'\nimport { BaseResource } from '../../adapters/index.js'\nimport {\n "
},
{
"path": "src/backend/utils/resources-factory/index.ts",
"chars": 69,
"preview": "export { default as ResourcesFactory } from './resources-factory.js'\n"
},
{
"path": "src/backend/utils/resources-factory/resources-factory.spec.js",
"chars": 4902,
"preview": "import { expect } from 'chai'\n\nimport { ResourcesFactory } from './resources-factory.js'\nimport { BaseDatabase, BaseReso"
},
{
"path": "src/backend/utils/resources-factory/resources-factory.ts",
"chars": 5294,
"preview": "import BaseResource from '../../adapters/resource/base-resource.js'\nimport AdminJS, { Adapter } from '../../../adminjs.j"
},
{
"path": "src/backend/utils/router/index.ts",
"chars": 28,
"preview": "export * from './router.js'\n"
},
{
"path": "src/backend/utils/router/router.doc.md",
"chars": 2302,
"preview": "Contains a list of all the routes used in AdminJS. They are grouped within 2 arrays:\n\n- `assets`\n- `routes`\n\nIt is used "
},
{
"path": "src/backend/utils/router/router.spec.ts",
"chars": 482,
"preview": "import { expect } from 'chai'\n\nimport Router from './router.js'\n\ndescribe('Router', function () {\n it('has both assets "
},
{
"path": "src/backend/utils/router/router.ts",
"chars": 5090,
"preview": "import path from 'path'\nimport * as url from 'url'\nimport { createRequire } from 'node:module'\n\nimport AppController fro"
},
{
"path": "src/backend/utils/uploaded-file.type.ts",
"chars": 556,
"preview": "/**\n * File uploaded via FormData to the backend.\n *\n * @memberof AdminJS\n * @alias UploadedFile\n */\nexport type Uploade"
},
{
"path": "src/backend/utils/view-helpers/index.ts",
"chars": 34,
"preview": "export * from './view-helpers.js'\n"
},
{
"path": "src/backend/utils/view-helpers/view-helpers.spec.ts",
"chars": 783,
"preview": "import { expect } from 'chai'\n\nimport ViewHelpers from './view-helpers.js'\n\ndescribe('ViewHelpers', function () {\n desc"
},
{
"path": "src/backend/utils/view-helpers/view-helpers.ts",
"chars": 8003,
"preview": "import { AdminJSOptions, Assets } from '../../../adminjs-options.interface.js'\nimport type { PathsInState } from '../../"
},
{
"path": "src/constants.ts",
"chars": 227,
"preview": "/* cspell: disable */\nexport const DOCS = 'https://docs.adminjs.co'\nexport const DEFAULT_PATHS = {\n rootPath: '/admin',"
},
{
"path": "src/core-scripts.interface.ts",
"chars": 915,
"preview": "/**\n * @memberof Assets\n * @alias CoreScripts\n *\n * Optional mapping of core AdminJS browser scripts:\n * - app.bundle.js"
},
{
"path": "src/current-admin.interface.ts",
"chars": 789,
"preview": "/**\n * Currently logged in admin.\n *\n * ### Usage with TypeScript\n *\n * ```typescript\n * import { CurrentAdmin } from 'a"
},
{
"path": "src/frontend/assets/styles/icomoon.css",
"chars": 1733,
"preview": "@font-face {\n\tfont-family: 'icomoon';\n\tsrc: url(\"icomoon.eot\");\n\tsrc: url(\"icomoon.eot#iefix\") format(\"embedded-opentype"
},
{
"path": "src/frontend/bundle-entry.jsx",
"chars": 2471,
"preview": "import { ThemeProvider } from '@adminjs/design-system/styled-components'\nimport React, { Suspense } from 'react'\nimport "
},
{
"path": "src/frontend/components/actions/action.props.ts",
"chars": 782,
"preview": "import { Dispatch, SetStateAction } from 'react'\n\nimport { ActionJSON, RecordJSON, ResourceJSON } from '../../interfaces"
},
{
"path": "src/frontend/components/actions/bulk-delete.tsx",
"chars": 4045,
"preview": "import { Button, DrawerContent, DrawerFooter, Icon, MessageBox, Table, TableBody, TableCell, TableRow, Text } from '@adm"
},
{
"path": "src/frontend/components/actions/edit.tsx",
"chars": 3257,
"preview": "import { Box, Button, DrawerContent, DrawerFooter, Icon } from '@adminjs/design-system'\nimport React, { FC, useEffect } "
},
{
"path": "src/frontend/components/actions/index.ts",
"chars": 488,
"preview": "import { New } from './new.js'\nimport { Edit } from './edit.js'\nimport { Show } from './show.js'\nimport { List } from '."
},
{
"path": "src/frontend/components/actions/list.tsx",
"chars": 2612,
"preview": "import { Box, Pagination, Text } from '@adminjs/design-system'\nimport React, { useEffect } from 'react'\nimport { useLoca"
},
{
"path": "src/frontend/components/actions/new.tsx",
"chars": 4649,
"preview": "import { Box, Button, DrawerContent, DrawerFooter, Icon } from '@adminjs/design-system'\nimport pick from 'lodash/pick.js"
},
{
"path": "src/frontend/components/actions/show.tsx",
"chars": 1602,
"preview": "import { DrawerContent } from '@adminjs/design-system'\nimport React from 'react'\n\nimport allowOverride from '../../hoc/a"
},
{
"path": "src/frontend/components/actions/utils/append-force-refresh.spec.ts",
"chars": 2180,
"preview": "import { expect } from 'chai'\n\nimport { appendForceRefresh } from './append-force-refresh.js'\n\ndescribe('appendForceRefr"
},
{
"path": "src/frontend/components/actions/utils/append-force-refresh.ts",
"chars": 1490,
"preview": "export const REFRESH_KEY = 'refresh'\nexport const IGNORE_PARAMS_KEY = 'ignore_params'\n\n/**\n * Adds refresh=true to the u"
},
{
"path": "src/frontend/components/actions/utils/index.ts",
"chars": 45,
"preview": "export * from './layout-element-renderer.js'\n"
},
{
"path": "src/frontend/components/actions/utils/layout-element-renderer.tsx",
"chars": 2359,
"preview": "import React from 'react'\nimport * as DesignSystem from '@adminjs/design-system'\n\nimport { ActionProps } from '../action"
},
{
"path": "src/frontend/components/app/action-button/action-button.tsx",
"chars": 2416,
"preview": "import React, { ReactElement } from 'react'\nimport { stringify } from 'qs'\n\nimport { ActionResponse } from '../../../../"
},
{
"path": "src/frontend/components/app/action-button/index.ts",
"chars": 35,
"preview": "export * from './action-button.js'\n"
},
{
"path": "src/frontend/components/app/action-header/action-header-props.tsx",
"chars": 821,
"preview": "import { ActionJSON, RecordJSON, ResourceJSON } from '../../../interfaces/index.js'\nimport { ActionResponse } from '../."
},
{
"path": "src/frontend/components/app/action-header/action-header.tsx",
"chars": 4991,
"preview": "/* eslint-disable jsx-a11y/anchor-is-valid */\nimport { Badge, Box, ButtonGroup, cssClass, H2, H3 } from '@adminjs/design"
},
{
"path": "src/frontend/components/app/action-header/actions-to-button-group.spec.ts",
"chars": 3822,
"preview": "import { ButtonGroupProps } from '@adminjs/design-system'\nimport { expect } from 'chai'\nimport i18n from 'i18next'\nimpor"
},
{
"path": "src/frontend/components/app/action-header/actions-to-button-group.ts",
"chars": 2002,
"preview": "import { ButtonGroupProps, ButtonInGroupProps } from '@adminjs/design-system'\n\nimport { actionHref, ActionJSON, buildAct"
},
{
"path": "src/frontend/components/app/action-header/index.ts",
"chars": 76,
"preview": "export * from './action-header.js'\nexport * from './action-header-props.js'\n"
},
{
"path": "src/frontend/components/app/action-header/styled-back-button.tsx",
"chars": 1281,
"preview": "import React from 'react'\nimport { Link as RouterLink } from 'react-router-dom'\nimport { useLocation } from 'react-route"
},
{
"path": "src/frontend/components/app/admin-modal.tsx",
"chars": 383,
"preview": "import { Modal } from '@adminjs/design-system'\nimport React, { FC } from 'react'\nimport { useSelector } from 'react-redu"
},
{
"path": "src/frontend/components/app/app-loader.tsx",
"chars": 235,
"preview": "import { Box, Loader } from '@adminjs/design-system'\nimport React, { FC } from 'react'\n\nexport const AppLoader: FC = () "
},
{
"path": "src/frontend/components/app/auth-background-component.tsx",
"chars": 525,
"preview": "import React from 'react'\n\nimport allowOverride from '../../hoc/allow-override.js'\n\nconst AuthenticationBackgroundCompon"
},
{
"path": "src/frontend/components/app/base-action-component.tsx",
"chars": 2525,
"preview": "import React from 'react'\nimport { Trans } from 'react-i18next'\nimport { MessageBox, Link } from '@adminjs/design-system"
},
{
"path": "src/frontend/components/app/breadcrumbs.tsx",
"chars": 3288,
"preview": "import { Box, cssClass, Text } from '@adminjs/design-system'\nimport { styled } from '@adminjs/design-system/styled-compo"
},
{
"path": "src/frontend/components/app/default-dashboard.tsx",
"chars": 5879,
"preview": "import React from 'react'\nimport { Box, Button, H2, H5, Illustration, IllustrationProps, Text } from '@adminjs/design-sy"
},
{
"path": "src/frontend/components/app/drawer-portal.tsx",
"chars": 3596,
"preview": "import React, { useEffect, ReactNode, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimp"
},
{
"path": "src/frontend/components/app/error-boundary.tsx",
"chars": 966,
"preview": "import React, { ReactNode } from 'react'\nimport { Text, MessageBox } from '@adminjs/design-system'\n\nimport { useTranslat"
},
{
"path": "src/frontend/components/app/error-message.tsx",
"chars": 2303,
"preview": "import React, { ReactNode } from 'react'\nimport { InfoBox, MessageBox, Text } from '@adminjs/design-system'\n\nimport { us"
},
{
"path": "src/frontend/components/app/filter-drawer.tsx",
"chars": 4180,
"preview": "import { Box, Button, Drawer, DrawerContent, DrawerFooter, H3, Icon } from '@adminjs/design-system'\nimport isNil from 'l"
},
{
"path": "src/frontend/components/app/footer.tsx",
"chars": 282,
"preview": "import React from 'react'\n\nimport allowOverride from '../../hoc/allow-override.js'\n\nconst Footer: React.FC = () => null\n"
},
{
"path": "src/frontend/components/app/index.ts",
"chars": 755,
"preview": "export * from './action-button/index.js'\nexport * from './action-header/index.js'\nexport * from './admin-modal.js'\nexpor"
},
{
"path": "src/frontend/components/app/language-select/index.ts",
"chars": 37,
"preview": "export * from './language-select.js'\n"
},
{
"path": "src/frontend/components/app/language-select/language-select.tsx",
"chars": 1413,
"preview": "import {\n Box,\n Button,\n DropDown,\n DropDownItem,\n DropDownMenu,\n DropDownTrigger,\n Icon,\n} from '@adminjs/design"
},
{
"path": "src/frontend/components/app/logged-in.tsx",
"chars": 1182,
"preview": "import React from 'react'\nimport { CurrentUserNav, Box, CurrentUserNavProps } from '@adminjs/design-system'\n\nimport { Cu"
}
]
// ... and 294 more files (download for full content)
About this extraction
This page contains the full source code of the SoftwareBrothers/adminjs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 494 files (1.0 MB), approximately 336.8k tokens, and a symbol index with 564 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.