decap-cms icon indicating copy to clipboard operation
decap-cms copied to clipboard

feat: dependent fields

Open barthc opened this issue 5 years ago • 16 comments

Closes #565

Based on comment https://github.com/netlify/netlify-cms/issues/565#issuecomment-506787922, came up with the following:

  • Added support for a new field config option conditions which should hold an array of objects.
  • Each object should have a fieldPath attribute to reference the field, wildcards * are supported as well similar to relation widget attrs. And any one of equal ,notEqual , oneOf and pattern config option.

So based on the issue's OP, a config like so would work.

- label: 'structure'
  name: structure
  widget: 'list'
  fields: 
    - {label: "type", name: "type", widget: "select", default: '', options: ['copy', 'divider', 'image', 'infographic', 'productCircle']}
    - label: content
      name: content
      widget: object
      fields:
        - {label: 'text', name: 'text', widget: 'markdown', conditions: [{ fieldPath: 'structure.*.type', oneOf: ['divider', 'productCircle', infographic, image, ''] }] }
        - {label: 'src', name: 'src', widget: 'image', conditions: [{ fieldPath: 'structure.*.type', oneOf: ['divider', 'productCircle', copy, ''] }] }
        - {label: 'overlay', name: 'overlay', widget: 'image', conditions: [{ fieldPath: 'structure.*.type', oneOf: ['divider', 'productCircle', copy, image, ''] }] }

Example config that tries to mimic dynamic dropdown:

- {label: "Continent", name: "continent", widget: "select", default: '', options: ['Europe', 'Asia']}
- {label: "Europe", name: "countries_europe", widget: "select", options: ['spain', 'italy', 'france'], conditions: [{ fieldPath: 'continent', oneOf: ['Asia', ''] }] }
- {label: "Asia", name: "countries_asia", widget: "select", options: ['india', 'japan', 'china'], conditions: [{ fieldPath: 'continent', oneOf: ['Europe', ''] }] }

To do:

  • [ ] Add cypress test
  • [ ] Update docs

barthc avatar Jun 12 '20 13:06 barthc

Looks like the field will be hidden when the condition is met and not shown only when the condition is met. Am I correct?

The field will be hidden when any(not all) of the conditions are met, I think we should change conditions config to something like hideConditions for clarity.

Doing the following seems more intuitive to me (and matches #565 (comment)):

The empty string is just to hide both Europe and Asia dropdown( and then show the appropriate dropdown as the continent dropdown value changes) for new entries since the default value of the continent dropdown is an empty string, the empty string can be omitted, it's up to the user.

Using the wildcard * is great for supporting lists. I wonder if we should create a dedicated data structure to hold the tree of widgets since we already pass parentIds for the nested validation.

Not sure how the tree structure will be useful here if you can explain with some codes, better. The fieldPath value is used directly on the entry data. The trick here is for the user to match the fieldPath correctly especially when using wildcard for list.

barthc avatar Jun 23 '20 00:06 barthc

The field will be hidden when any(not all) of the conditions are met, I think we should change conditions config to something like hideConditions for clarity.

With hide notation each time someone adds a select option it will require editing all other fields or the condition will break right?

With show notation you'd only need to do:

- {label: "Continent", name: "continent", widget: "select", default: '', options: ['Europe', 'Asia']}
- {label: "Europe", name: "countries_europe", widget: "select", options: ['spain', 'italy', 'france'], conditions: [{ fieldPath: 'continent', equals: 'Europe' }] }
- {label: "Asia", name: "countries_asia", widget: "select", options: ['india', 'japan', 'china'], conditions: [{ fieldPath: 'continent', equals: 'Asia' }] }

Then adding a new option:

- {label: "Continent", name: "continent", widget: "select", default: '', options: ['Europe', 'Asia','America']}
- {label: "Europe", name: "countries_europe", widget: "select", options: ['spain', 'italy', 'france'], conditions: [{ fieldPath: 'continent', equal: 'Europe' }] }
- {label: "Asia", name: "countries_asia", widget: "select", options: ['india', 'japan', 'china'], conditions: [{ fieldPath: 'continent', equal: 'Asia' }] }
- {label: "America", name: "countries_america", widget: "select", options: ['mexico', 'us'], conditions: [{ fieldPath: 'continent', equal: 'America' }] }

Not sure how the tree structure will be useful here if you can explain with some codes, better. The fieldPath value is used directly on the entry data. The trick here is for the user to match the fieldPath correctly especially when using wildcard for list.

Let me dig into that a bit more.

erezrokah avatar Jun 23 '20 16:06 erezrokah

With hide notation each time someone adds a select option it will require editing all other fields or the condition will break right?

With show notation you'd only need to do:

Agreed.

barthc avatar Jun 23 '20 22:06 barthc

When will this feature will be available?

miguelt1 avatar Mar 11 '21 16:03 miguelt1

When will this feature will be available?

See my comment here https://github.com/netlify/netlify-cms/issues/565#issuecomment-792674198

erezrokah avatar Mar 11 '21 16:03 erezrokah

Closing this as stale. We can revisit in the future, or if someone wants to pick it up per https://github.com/netlify/netlify-cms/pull/3891#pullrequestreview-434938425

erezrokah avatar Apr 06 '21 14:04 erezrokah

Hey there! I'm in need of this so happy to try to pick this up again from https://github.com/decaporg/decap-cms/pull/3891#pullrequestreview-434938425

larenelg avatar Mar 21 '23 06:03 larenelg

sooo is this not a feature? seems like a really basic need for ANY CMS lol

bobgravity1 avatar May 18 '23 10:05 bobgravity1

@larenelg I reopened this PR if you're still interested

martinjagodic avatar May 18 '23 12:05 martinjagodic

Could this use case be solved by making variable types work for object widgets? That's effectively what we do, by having lists with min and/or max set to 1.

It would have the benefits of being (presumably) easier to implement/maintain and easier to understand by users.

mmkal avatar May 18 '23 22:05 mmkal

@mmkal can you explain how that would enable the conditional feature? Maybe with an example? Thanks

martinjagodic avatar May 19 '23 07:05 martinjagodic

Sure, I'll have to adjust the original example, because the example from the OP, and proposed solution, is already a list so can just use variable types right now: https://github.com/decaporg/decap-cms/issues/565#issuecomment-506787922

- label: structure
  name: structure
  widget: list
  types:
     - name: copy
       widget: object
       fields:
       - name: text
         widget: markdown
     - name: productCircle
       widget: object
       fields:
       - name: text
         widget: markdown
       - name: src
         widget: image
     - name: infographic
       widget: object
       fields:
       - name: text
         widget: markdown
       - name: src
         widget: image
       - name: overlay
         widget: image
       ...

I think this is much easier to follow when looking at the config too. Answering the question "what subfields does an infographic have" doesn't involve darting around and looking at all of the condition labels, and mentally parsing the custom condition syntax (which has to be learned and remembered).

Worth noting, there is one slight downside which is that fields in common between the various types have to be defined explicitly on each. That can be solved fairly easily with yaml references (or my team generate the config from typescript anyway so we can use helper methods and strong types), but also more importantly, by the same token, it allows for the different types to independently define their fields. markdown on productCircle might have a different hint than markdown on infographic.

So, IMO the original issue could be closed with a suggestion like this. But if someone wanted to make a non-list field like this, that is a missing feature. It'd be nice to be able to do the same thing for objects, e.g.:

- label: structure
  name: structure
  widget: object # 👈 not supported right now, only `widget: list` is 
  types:
    - name: copy
      widget: object
      fields:
        - name: text
          widget: markdown
    - name: productCircle
      widget: object
      fields:
        - name: text
          widget: markdown
        - name: src
          widget: image
    - name: infographic
      widget: object
      fields:
        - name: text
          widget: markdown
        - name: src
          widget: image
        - name: overlay
          widget: image
      ...

I think the use case for that is smaller, but it could make sense on folder collections - different entries could use different structures with the example above.

mmkal avatar May 19 '23 08:05 mmkal

Thanks for the comment @mmkal. This is a feature that has been discussed many times with many proposed solutions, so I would like to take some time with the team to carefully review them and decide where we want to go. It is one of the most requested features, so it's important not to rush.

martinjagodic avatar May 22 '23 08:05 martinjagodic

any updates? come on

ed-ponce avatar Nov 23 '23 16:11 ed-ponce

Here's something we've started doing, along the lines of my above comment, but also adds support for objects. Basically, just inspect the field and use a shimmed list widget control instead of object when the types field is defined:

import {List} from 'immutable'
import React from 'react'
import CMS from 'decap-cms-app'

export function optionalizeObject() {
  const listWidget = CMS.getWidget('list')
  const objectWidget = CMS.getWidget('object')

  class NewObjectControl extends React.Component<any> {
    render() {
      if (this.props.field.get('required') === false || this.props.field.get('types')) {
        return (
          <listWidget.control
            {...this.props}
            onChange={(e: List<any>) => this.props.onChange(e.get(0))}
            value={List([this.props.value].filter(Boolean))}
          />
        )
      }
      return <objectWidget.control {...this.props} />
    }
  }

  CMS.registerWidget('object', NewObjectControl, listWidget.preview)
}

Here's a demo:

collection definition:

{
  label: 'test test',
  name: 'test',
  folder: 'shared/content/src/data/marketing/test',
  slug: '{{slug}}',
  format: 'yml',
  create: true,
  fields: [
    {
      name: 'some_string_field',
      widget: 'string',
    },
    {
      name: 'structure',
      widget: 'object',
      types: [
        {
          name: 'copy',
          widget: 'object',
          fields: [
            {name: 'text', widget: 'markdown'},
          ],
        },
        {
          name: 'productCircle',
          widget: 'object',
          fields: [
            {name: 'text', widget: 'markdown'},
            {name: 'src', widget: 'image'},
          ],
        },
        {
          name: 'infographic',
          widget: 'object',
          fields: [
            {name: 'text', widget: 'markdown'},
            {name: 'src', widget: 'image'},
            {name: 'overlay', widget: 'image'},
          ],
        },
      ],
    }
  ]
},

How it renders in the UI:

https://github.com/decaporg/decap-cms/assets/15040698/76d8df8d-be2a-4a6e-88d6-52e70aa7a653

Files generated by the above:

image

Bonus: this also improves support for object widgets with required: false - they become "lists" too, visually - the add/remove buttons define the object and set it to null respectively. Before this, we had trouble with objects that were optional, but had required subfields, when defined.


In the spirit of not adding complexity to both the product and the codebase, I think the above would be a better solution than this pull request, so I would propose the following changes instead:

  1. Make the above shim built into decapcms, so it doesn't have to be done in userland. This should be easy to do, and improves support for optional object, for all users, as a bonus.
  2. Allow for types to be define on folder collections, instead of requiring fields. Without this, there's a limitation of the above method that you can't have the type field on the top level (i.e. it has to be {structure: {type: copy, text: abc}} and can't be {type: copy, text: abc}. If we're lucky this wouldn't be too hard to achieve either.

1 can be done first, then 2. I'd be happy to open a PR.

@martinjagodic what do you think?

mmkal avatar Jan 11 '24 16:01 mmkal

@mmkal I like this a lot! It achieves a lot with very little intervention. A PR for solution 1 would be amazing.

martinjagodic avatar Jan 12 '24 12:01 martinjagodic