SoftwareBrothers / adminjs

AdminJS is an admin panel for apps written in node.js
https://adminjs.co
MIT License
8.07k stars 650 forks source link

[Bug]: I have 2 dropdowns, is there any way to filter out data in 2nd dropdown on the basis of value selected in 1st dropdown in Admin JS library? #1625

Closed harshagr64 closed 4 months ago

harshagr64 commented 5 months ago

Contact Details

No response

What happened?

List what you are trying to do?

Bug prevalence

Multiple Times

AdminJS dependencies version

7.2.0

What browsers do you see the problem on?

No response

Relevant log output

No response

Relevant code that's giving you issues

No response

dziraf commented 5 months ago

You can create a custom component for the other dropdown and filter out results based on props.record.params. props.record holds the state of the form

harshagr64 commented 5 months ago

@dziraf, can you give me a sample code on how can I achieve this?

harshagr64 commented 5 months ago

is it possible to create custom component for specific dropdown instead of creating custom component for the complete screen in Admin js?

Please help me as I am new to the admin js & node js.

dziraf commented 5 months ago

Yes, I'll help you tomorrow morning when I'm at work. Currently on a phone and it's inconvenient to write code

harshagr64 commented 5 months ago

Sure @dziraf, Thanks!!

dziraf commented 5 months ago

This is the default Reference component for edit action:

https://github.com/SoftwareBrothers/adminjs/blob/master/src/frontend/components/property-type/reference/edit.tsx

Options are loaded on these lines:

  const loadOptions = async (inputValue: string): Promise<SelectRecordEnhanced[]> => {
    const api = new ApiClient()

    const optionRecords = await api.searchRecords({
      resourceId,
      query: inputValue,
    })
    return optionRecords.map((optionRecord: RecordJSON) => ({
      value: optionRecord.id,
      label: optionRecord.title,
      record: optionRecord,
    }))
  }

Let's say you have 2 dropdowns:

Subcategory should only display categories belonging to chosen Category. The above function would look this way then:

  const loadOptions = async (inputValue: string): Promise<SelectRecordEnhanced[]> => {
    const api = new ApiClient()

    const optionRecords = await api.searchRecords({
      resourceId,
      query: inputValue,
      params: {
        'filters.categoryId': props.record?.params?.categoryId,
      }
    })
    return optionRecords.map((optionRecord: RecordJSON) => ({
      value: optionRecord.id,
      label: optionRecord.title,
      record: optionRecord,
    }))
  }

Full component:

import React, { FC, useState, useEffect, useMemo, memo } from 'react'
import { FormGroup, FormMessage, SelectAsync, Label } from '@adminjs/design-system'
import { ApiClient, EditPropertyProps, RecordJSON, flat, useTranslation } from 'adminjs'

type CombinedProps = EditPropertyProps
type SelectRecordEnhanced = {
  record: RecordJSON;
  label: string;
  value: any;
}

const EditReference: FC<CombinedProps> = (props) => {
  const { tp } = useTranslation()
  const { onChange, property, record } = props
  const { reference: resourceId } = property

  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,
      params: {
        'filters.categoryId': props.record?.params?.categoryId,
      }
    })
    return optionRecords.map((optionRecord: RecordJSON) => ({
      value: optionRecord.id,
      label: optionRecord.title,
      record: optionRecord,
    }))
  }
  const error = record?.errors[property.path]

  const selectedId = useMemo(
    () => flat.get(record?.params, property.path) as string | undefined,
    [record],
  )
  const [loadedRecord, setLoadedRecord] = useState<RecordJSON | undefined>()
  const [loadingRecord, setLoadingRecord] = useState(0)

  useEffect(() => {
    if (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)
      })
    }
  }, [selectedId, resourceId])

  const selectedValue = loadedRecord
  const selectedOption = (selectedId && selectedValue) ? {
    value: selectedValue.id,
    label: selectedValue.title,
  } : {
    value: '',
    label: '',
  }

  return (
    <FormGroup error={Boolean(error)}>
      <Label property={property} >{tp('property.path'), property.resourceId}</Label>
      <SelectAsync
        cacheOptions
        value={selectedOption}
        defaultOptions
        loadOptions={loadOptions}
        onChange={handleChange}
        isClearable
        isDisabled={property.isDisabled}
        isLoading={!!loadingRecord}
        {...property.props}
      />
      <FormMessage>{error?.message}</FormMessage>
    </FormGroup>
  )
}

export default EditReference

To replace the default component you should use ComponentLoader: https://docs.adminjs.co/ui-customization/writing-your-own-components

harshagr64 commented 5 months ago

Getting this error with your code: Object literal may only specify known properties, and 'params' does not exist in type '{ resourceId: string; query: string; searchProperty?: string; }'.

` const loadOptions = async (inputValue: string): Promise<SelectRecordEnhanced[]> => { const api = new ApiClient()

const optionRecords = await api.searchRecords({
  resourceId,
  query: inputValue,
  params: {
    'filters.categoryId': props.record?.params?.categoryId,
  }
})
return optionRecords.map((optionRecord: RecordJSON) => ({
  value: optionRecord.id,
  label: optionRecord.title,
  record: optionRecord,
}))

} const error = record?.errors[property.path] `

harshagr64 commented 5 months ago

@dziraf please help me with the error.

harshagr64 commented 5 months ago

Can anyone help me with this issue?

Thanks!!

dziraf commented 5 months ago

change

  const loadOptions = async (inputValue: string): Promise<SelectRecordEnhanced[]> => {
    const api = new ApiClient()

    const optionRecords = await api.searchRecords({
      resourceId,
      query: inputValue,
      params: {
        'filters.categoryId': props.record?.params?.categoryId,
      }
    })
    return optionRecords.map((optionRecord: RecordJSON) => ({
      value: optionRecord.id,
      label: optionRecord.title,
      record: optionRecord,
    }))
  }

to

  const loadOptions = async (inputValue: string): Promise<SelectRecordEnhanced[]> => {
    const api = new ApiClient()

    const response = await api.resourceAction({
      resourceId,
      actionName: 'search',
      query: inputValue,
      params: {
        'filters.categoryId': props.record?.params?.categoryId,
      }
    })
    const { records: optionRecords } = response.data

    return optionRecords.map((optionRecord: RecordJSON) => ({
      value: optionRecord.id,
      label: optionRecord.title,
      record: optionRecord,
    }))
  }