adminjs-typeorm
adminjs-typeorm copied to clipboard
Is there a way to add referenceColumnName in adminjs config?
Hello I have the following codes in my entity:
@ManyToOne(() => OrganizationEntity, (organization) => organization.code)
@JoinColumn({ name: 'organizationCode', referencedColumnName: 'code' })
organization: OrganizationEntity;
@Column({ name: 'organizationCode' })
organizationCode: number;
now in admin dashboard, "Adminjs" does not recognize "referencedColumnName" and it is recognizing it as "organizationId" while listing, creating, editing.
Is there a way to add "referenceColumnName" in "Adminjs" config?
Currently all references relate to primary keys, in one of the projects I've added populateReference
feature, but it's for Sequelize. I believe you can modify this to work with Typeorm though. I'll label this issue as enhancement
since supporting this in the core makes a lot of sense.
resourceReferenceFeature
:
import {
ActionContext,
ActionRequest,
ActionResponse,
BaseRecord,
BaseResource,
buildFeature,
FeatureType,
Filter
} from 'adminjs'
import CustomComponents from '../frontend/components/components'
export type ResourceReferenceArgs = {
foreignKey: string;
targetKey?: string | null;
reference: string | ((context: ActionContext) => string | Promise<string>);
}
export type ResourceReferencePopulatorArgs = ResourceReferenceArgs & { targetKey: string }
type FetchAdapterRecordsArgs = {
resource: BaseResource,
targetKey: string,
foreignValues: any[],
}
const getAdapterRecords = async ({
resource,
targetKey,
foreignValues,
}: FetchAdapterRecordsArgs) => {
const filter = new Filter({
[targetKey]: foreignValues
}, resource)
return resource.find(filter, {})
}
const getSequelizeAdapterRecords = async ({
resource,
targetKey,
foreignValues
}: FetchAdapterRecordsArgs) => {
// SequelizeModel is specific to sequelize adapter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const referencedModel = (resource as any).SequelizeModel
const referencedRecords = await referencedModel.findAll({
where: {
[targetKey]: foreignValues,
}
})
return referencedRecords.map((r) => new BaseRecord(r.toJSON(), resource))
}
export const populateReference = ({
foreignKey,
reference,
targetKey,
}: ResourceReferencePopulatorArgs) => async (
response: ActionResponse,
request: ActionRequest,
context: ActionContext,
): Promise<ActionResponse> => {
if (!targetKey) return response
const { _admin, currentAdmin } = context
const _reference = typeof reference === 'function'
? await reference(context)
: reference
const referencedResource = _admin.findResource(_reference)
if (!referencedResource) {
throw new Error(`Could not find resource with id: ${_reference}`)
}
let records
if (response.records?.length) records = response.records
if (response.record) records = [response.record]
if (!records || !records.length) {
return response
}
const foreignValues = records.map(({ params }) => params[foreignKey])
const getRecords = (referencedResource as any).SequelizeModel
? getSequelizeAdapterRecords
: getAdapterRecords
const referencedRecords = await getRecords({
resource: referencedResource,
targetKey,
foreignValues,
})
const referencedRecordsGroupedByTargetKeyId = referencedRecords.reduce((memo, record) => {
memo[record.params[targetKey]] = record
return memo
}, {})
records.forEach((record) => {
const foreignId = record.params[foreignKey]
if (referencedRecordsGroupedByTargetKeyId[foreignId]) {
const referenceRecord = referencedRecordsGroupedByTargetKeyId[foreignId].toJSON(currentAdmin)
record.populated[foreignKey] = referenceRecord
}
})
if (response.records?.length) response.records = records
if (response.record) response.record = records[0]
return response
}
export const resourceReference = ({
reference,
foreignKey,
targetKey = null,
}: ResourceReferenceArgs): FeatureType => {
if (!targetKey) {
return buildFeature({
properties: {
[foreignKey]: {
type: 'reference',
reference: reference as string,
}
},
})
}
return buildFeature({
properties: {
[foreignKey]: {
type: 'string',
components: {
edit: CustomComponents.ResourceReferenceSearchEdit,
show: CustomComponents.ResourceReferenceSearchShow,
},
custom: {
targetKey,
foreignKey,
reference,
}
}
},
actions: {
show: {
after: [populateReference({ foreignKey, targetKey, reference })]
},
edit: {
after: [populateReference({ foreignKey, targetKey, reference })]
},
new: {
after: [populateReference({ foreignKey, targetKey, reference })]
},
list: {
after: [populateReference({ foreignKey, targetKey, reference })]
},
search: {
after: [populateReference({ foreignKey, targetKey, reference })]
},
}
})
}
ResourceReferenceSearchEdit.tsx
:
import React, { FC, useState, useEffect } from 'react'
import Select from 'react-select/async'
import { FormGroup, FormMessage } from '@adminjs/design-system'
import { ApiClient, EditPropertyProps, SelectRecord, RecordJSON } from 'adminjs'
import PropertyLabel from '../property-label/property-label'
type SelectRecordEnhanced = SelectRecord & {
record: RecordJSON;
}
const ResourceReferenceSearchEdit: FC<EditPropertyProps > = (props) => {
const { onChange, property, record } = props
const { custom = {} } = property
const { targetKey, reference: resourceId } = custom
if (!resourceId) {
throw new Error(`Cannot reference resource in property '${property.path}'`)
}
const handleChange = (selected: SelectRecordEnhanced): void => {
if (selected) {
onChange(property.path, selected.value, selected.record)
} else {
onChange(property.path, null)
}
}
const loadOptions = async (inputValue: string): Promise<SelectRecordEnhanced[]> => {
const api = new ApiClient()
const optionRecords = await api.searchRecords({
resourceId,
query: inputValue,
})
return optionRecords.map((optionRecord: RecordJSON) => {
const value = targetKey ? optionRecord.params?.[targetKey] : optionRecord.id
return {
value,
label: optionRecord.title,
record: optionRecord,
}
})
}
const error = record?.errors[property.path]
const selectedId = record?.params[property.path] as string | undefined
const [loadedRecord, setLoadedRecord] = useState<RecordJSON | undefined>()
const [loadingRecord, setLoadingRecord] = useState(0)
const selectedValue = record?.populated[property.path] ?? loadedRecord
const selectedOption = (selectedId && selectedValue) ? {
value: selectedValue.id,
label: selectedValue.title,
} : {
value: '',
label: '',
}
useEffect(() => {
if (!selectedValue && selectedId) {
setLoadingRecord(c => c + 1)
const api = new ApiClient()
api.recordAction({
actionName: 'show',
resourceId,
recordId: selectedId,
}).then(({ data }: any) => {
setLoadedRecord(data.record)
}).finally(() => {
setLoadingRecord(c => c - 1)
})
}
}, [selectedValue, selectedId, resourceId])
return (
<FormGroup error={Boolean(error)}>
<PropertyLabel property={property} />
<Select
cacheOptions
value={selectedOption}
defaultOptions
loadOptions={loadOptions}
onChange={handleChange}
isClearable
isDisabled={property.isDisabled}
isLoading={!!loadingRecord}
{...property.props}
/>
<FormMessage>{error?.message}</FormMessage>
</FormGroup>
)
}
export default ResourceReferenceSearchEdit
ResourceReferenceSearchShow.tsx
:
import React from 'react'
import { ValueGroup } from '@adminjs/design-system'
import { ShowPropertyProps } from 'adminjs'
import ReferenceValue from './reference-value'
const ResourceReferenceSearchShow: React.FC<ShowPropertyProps> = (props) => {
const { property, record } = props
return (
<ValueGroup label={property.label}>
<ReferenceValue property={property} record={record} />
</ValueGroup>
)
}
export default ResourceReferenceSearchShow
ReferenceValue.tsx
:
import React from 'react'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
import { ButtonCSS } from '@adminjs/design-system'
import { ViewHelpers, RecordJSON, PropertyJSON } from 'adminjs'
interface Props {
property: PropertyJSON;
record: RecordJSON;
}
const StyledLink = styled<any>(Link)`
${ButtonCSS};
padding-left: ${({ theme }): string => theme.space.xs};
padding-right: ${({ theme }): string => theme.space.xs};
`
const ReferenceValue: React.FC<Props> = (props) => {
const { property, record } = props
const { custom = {} } = property
const { reference, targetKey } = custom
const h = new ViewHelpers()
const populated = record.populated[property.path]
const value = (populated && populated.title)
const refId = populated?.id ?? populated?.params?.[targetKey]
if (!reference) {
throw new Error(`property: "${property.path}" does not have a reference`)
}
if (populated && populated.recordActions.find(a => a.name === 'show')) {
const href = h.recordActionUrl({
resourceId: reference, recordId: refId, actionName: 'show',
})
return (
<StyledLink variant="text" to={href}>{value}</StyledLink>
)
}
return (
<span>{value}</span>
)
}
export default ReferenceValue
Usage in resource:
...,
features: [
resourceReference({ foreignKey: 'entityUid', targetKey: 'uid', reference: 'entities-entity' }),
]
This feature is urgently needed