Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a way to add referenceColumnName in adminjs config? #39

Open
pejmanhadavi opened this issue Apr 4, 2022 · 2 comments
Open
Labels
enhancement New feature or request

Comments

@pejmanhadavi
Copy link

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?

@dziraf
Copy link

dziraf commented Apr 5, 2022

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' }),
  ]

@dziraf dziraf added the enhancement New feature or request label Apr 5, 2022
@zenghj
Copy link

zenghj commented Mar 7, 2023

This feature is urgently needed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants